Loading app/src/androidTest/java/at/bitfire/davdroid/DateUtilsTest.java 0 → 100644 +82 −0 Original line number Diff line number Diff line /* * 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 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; import java.text.ParseException; import java.util.ArrayList; import java.util.List; public class DateUtilsTest extends TestCase { private static final String tzIdVienna = "Europe/Vienna"; public void testRecurrenceSetsToAndroidString() throws ParseException { // one entry without time zone (implicitly UTC) final List<RDate> list = new ArrayList<>(2); list.add(new RDate(new DateList("20150101T103010Z,20150102T103020Z", Value.DATE_TIME))); assertEquals("20150101T103010Z,20150102T103020Z", DateUtils.recurrenceSetsToAndroidString(list, false)); // two entries (previous one + this), both with time zone Vienna list.add(new RDate(new DateList("20150103T113030,20150704T123040", Value.DATE_TIME))); final TimeZone tz = DateUtils.tzRegistry.getTimeZone(tzIdVienna); for (RDate rdate : list) rdate.setTimeZone(tz); assertEquals("20150101T103010Z,20150102T103020Z,20150103T103030Z,20150704T103040Z", DateUtils.recurrenceSetsToAndroidString(list, false)); // DATEs (without time) have to be converted to <date>T000000Z for Android list.clear(); list.add(new RDate(new DateList("20150101,20150702", Value.DATE))); assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true)); // DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to <date>T000000Z for Android list.clear(); list.add(new RDate(new DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME))); assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true)); } public void testAndroidStringToRecurrenceSets() throws ParseException { // list of UTC times ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, false); DateList exDates = exDate.getDates(); assertEquals(Value.DATE_TIME, exDates.getType()); assertTrue(exDates.isUtc()); assertEquals(2, exDates.size()); assertEquals(1420108210000L, exDates.get(0).getTime()); assertEquals(1435833020000L, exDates.get(1).getTime()); // list of time zone times exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(tzIdVienna + ";20150101T103010,20150702T103020", ExDate.class, false); exDates = exDate.getDates(); assertEquals(Value.DATE_TIME, exDates.getType()); assertEquals(DateUtils.tzRegistry.getTimeZone(tzIdVienna), exDates.getTimeZone()); assertEquals(2, exDates.size()); assertEquals(1420104610000L, exDates.get(0).getTime()); assertEquals(1435825820000L, exDates.get(1).getTime()); // list of dates exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, true); exDates = exDate.getDates(); assertEquals(Value.DATE, exDates.getType()); assertEquals(2, exDates.size()); assertEquals("20150101", exDates.get(0).toString()); assertEquals("20150702", exDates.get(1).toString()); } } app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.java +6 −21 Original line number Diff line number Diff line Loading @@ -30,17 +30,22 @@ 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; public class LocalCalendarTest extends InstrumentationTestCase { Loading Loading @@ -133,8 +138,7 @@ public class LocalCalendarTest extends InstrumentationTestCase { public void testBuildEntry() throws LocalStorageException, ParseException { final String vcardName = "testBuildEntry"; TimeZoneRegistry tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry(); TimeZone tzVienna = tzRegistry.getTimeZone("Europe/Vienna"); final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna"); assertNotNull(tzVienna); // build and write event to calendar provider Loading Loading @@ -231,23 +235,4 @@ public class LocalCalendarTest extends InstrumentationTestCase { } } public void testRecurrenceSetsToAndroidString() throws ParseException { final String tzId = "Europe/Vienna"; // one entry without time zone final List<RDate> list = new ArrayList<>(2); list.add(new RDate(new DateList("20150101T103000,20150102T103000", Value.DATE_TIME))); assertEquals("20150101T103000,20150102T103000", LocalCalendar.recurrenceSetsToAndroidString(list)); // two entries with time zone list.add(new RDate(new DateList("20150103T103000,20150104T103000", Value.DATE_TIME))); final TimeZoneRegistry tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry(); final TimeZone tz = tzRegistry.getTimeZone(tzId); for (RDate rdate : list) rdate.setTimeZone(tz); assertEquals(tzId + ";20150101T103000,20150102T103000,20150103T103000,20150104T103000", LocalCalendar.recurrenceSetsToAndroidString(list)); } } app/src/main/java/at/bitfire/davdroid/DateUtils.java +114 −3 Original line number Diff line number Diff line Loading @@ -11,19 +11,43 @@ 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"; private final static TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry(); public final static 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 tzID) { String localTZ = null; Loading Loading @@ -56,8 +80,95 @@ public class DateUtils { return localTZ; } public static TimeZone getTimeZone(String tzID) { return tzRegistry.getTimeZone(tzID); // recurrence sets /** * Concatenates, if necessary, multiple RDATE/EXDATE lists and converts them to * a formatted string which Android calendar provider can process. * Android expects this format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss" (when * TZID is given) or "yyyymmddThhmmssZ". We don't use the TZID format here because then we're limited * to one time-zone, while an iCalendar may contain multiple EXDATE/RDATE lines with different time zones. * @param dates one more more lists of RDATE or EXDATE * @param allDay indicates whether the event is an all-day event or not * @return formatted string for Android calendar provider: * - in case of all-day events, all dates/times are returned as yyyymmddT000000Z * - in case of timed events, all dates/times are returned as UTC time: yyyymmddThhmmssZ */ public static String recurrenceSetsToAndroidString(List<? extends DateListProperty> dates, boolean allDay) throws ParseException { List<String> strDates = new LinkedList<>(); /* rdate/exdate: DATE DATE_TIME all-day store as ...T000000Z cut off time and store as ...T000000Z event with time (ignored) store as ...ThhmmssZ */ final DateFormat dateFormatUtcMidnight = new SimpleDateFormat("yyyyMMdd'T'000000'Z'"); for (DateListProperty dateListProp : dates) { final Value type = dateListProp.getDates().getType(); if (Value.DATE_TIME.equals(type)) { // DATE-TIME values will be stored in UTC format for Android if (allDay) { DateList dateList = dateListProp.getDates(); for (Date date : dateList) strDates.add(dateFormatUtcMidnight.format(date)); } else { dateListProp.setUtc(true); strDates.add(dateListProp.getValue()); } } else if (Value.DATE.equals(type)) // DATE values have to be converted to DATE-TIME <date>T000000Z for Android for (Date date : dateListProp.getDates()) strDates.add(dateFormatUtcMidnight.format(date)); } return StringUtils.join(strDates, ","); } /** * Takes a formatted string as provided by the Android calendar provider and returns a DateListProperty * constructed from these values. * @param dbStr formatted string from Android calendar provider (RDATE/EXDATE field) * expected format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss[Z]" * @param type subclass of DateListProperty, e.g. RDate or ExDate * @param allDay true: list will contain DATE values; false: list will contain DATE_TIME values * @return instance of "type" containing the parsed dates/times from the string */ public static DateListProperty androidStringToRecurrenceSet(String dbStr, Class<? extends DateListProperty> type, boolean allDay) throws ParseException { // 1. split string into time zone and actual dates TimeZone timeZone; String datesStr; final int limiter = dbStr.indexOf(';'); if (limiter != -1) { // TZID given timeZone = DateUtils.tzRegistry.getTimeZone(dbStr.substring(0, limiter)); datesStr = dbStr.substring(limiter + 1); } else { timeZone = null; datesStr = dbStr; } // 2. process date string and generate list of DATEs or DATE-TIMEs DateList dateList; if (allDay) { dateList = new DateList(Value.DATE); for (String s: StringUtils.split(datesStr, ',')) dateList.add(new Date(new DateTime(s))); } else { dateList = new DateList(datesStr, Value.DATE_TIME, timeZone); if (timeZone == null) dateList.setUtc(true); } // 3. generate requested DateListProperty (RDate/ExDate) from list of DATEs or DATE-TIMEs DateListProperty list; try { list = (DateListProperty)type.getDeclaredConstructor(new Class[] { DateList.class } ).newInstance(dateList); if (dateList.getTimeZone() != null) list.setTimeZone(dateList.getTimeZone()); } catch (Exception e) { throw new ParseException("Couldn't create date/time list by reflection", -1); } return list; } } app/src/main/java/at/bitfire/davdroid/resource/Event.java +2 −2 Original line number Diff line number Diff line Loading @@ -321,7 +321,7 @@ public class Event extends iCalendar { dtStart = new DtStart(new Date(tsStart)); } else { DateTime start = new DateTime(tsStart); start.setTimeZone(tzRegistry.getTimeZone(tzID)); start.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID)); dtStart = new DtStart(start); } } Loading @@ -340,7 +340,7 @@ public class Event extends iCalendar { dtEnd = new DtEnd(new Date(tsEnd)); } else { DateTime end = new DateTime(tsEnd); end.setTimeZone(tzRegistry.getTimeZone(tzID)); end.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID)); dtEnd = new DtEnd(end); } } Loading app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java +17 −40 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ 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; Loading @@ -61,6 +62,7 @@ import java.net.URISyntaxException; import java.text.ParseException; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import at.bitfire.davdroid.DAVUtils; import at.bitfire.davdroid.DateUtils; Loading Loading @@ -349,8 +351,7 @@ public class LocalCalendar extends LocalCollection<Event> { String strRDate = values.getAsString(Events.RDATE); if (!StringUtils.isEmpty(strRDate)) { RDate rDate = new RDate(); rDate.setValue(strRDate); RDate rDate = (RDate)DateUtils.androidStringToRecurrenceSet(strRDate, RDate.class, allDay); e.getRdates().add(rDate); } Loading @@ -363,9 +364,7 @@ public class LocalCalendar extends LocalCollection<Event> { String strExDate = values.getAsString(Events.EXDATE); if (!StringUtils.isEmpty(strExDate)) { // always empty, see https://code.google.com/p/android/issues/detail?id=172411 ExDate exDate = new ExDate(); exDate.setValue(strExDate); ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(strExDate, ExDate.class, allDay); e.getExdates().add(exDate); } } catch (ParseException ex) { Loading Loading @@ -540,12 +539,20 @@ public class LocalCalendar extends LocalCollection<Event> { } if (!event.getRdates().isEmpty()) { recurring = true; builder.withValue(Events.RDATE, recurrenceSetsToAndroidString(event.getRdates())); 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.getExrule() != null) builder.withValue(Events.EXRULE, event.getExrule().getValue()); if (!event.getExdates().isEmpty()) builder.withValue(Events.EXDATE, recurrenceSetsToAndroidString(event.getExdates())); 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) Loading Loading @@ -731,34 +738,4 @@ public class LocalCalendar extends LocalCollection<Event> { return calendarsURI(account); } /** * Concatenates, if necessary, multiple RDATE/EXDATE lists and prepares * a formatted string as expected by Android calendar provider * @param dates one more more lists of RDATE or EXDATE * @return formatted string for Android calendar provider */ static String recurrenceSetsToAndroidString(List<? extends DateListProperty> dates) { String tzID = null; List<String> strDates = new LinkedList<>(); for (DateListProperty dateList : dates) { if (dateList.getTimeZone() != null) { String thisTzID = DateUtils.findAndroidTimezoneID(dateList.getTimeZone().getID()); if (tzID == null) tzID = thisTzID; else if (!tzID.equals(thisTzID)) Log.w(TAG, "Multiple EXDATEs/RDATEs with different time zones not supported by Android, using " + tzID + " for all dates"); } strDates.add(dateList.getValue()); } // Android expects this format: "[TZID;]date1,date2,date3" String dateStr = ""; if (tzID != null) dateStr += tzID + ";"; dateStr += StringUtils.join(strDates, ","); return dateStr; } } Loading
app/src/androidTest/java/at/bitfire/davdroid/DateUtilsTest.java 0 → 100644 +82 −0 Original line number Diff line number Diff line /* * 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 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; import java.text.ParseException; import java.util.ArrayList; import java.util.List; public class DateUtilsTest extends TestCase { private static final String tzIdVienna = "Europe/Vienna"; public void testRecurrenceSetsToAndroidString() throws ParseException { // one entry without time zone (implicitly UTC) final List<RDate> list = new ArrayList<>(2); list.add(new RDate(new DateList("20150101T103010Z,20150102T103020Z", Value.DATE_TIME))); assertEquals("20150101T103010Z,20150102T103020Z", DateUtils.recurrenceSetsToAndroidString(list, false)); // two entries (previous one + this), both with time zone Vienna list.add(new RDate(new DateList("20150103T113030,20150704T123040", Value.DATE_TIME))); final TimeZone tz = DateUtils.tzRegistry.getTimeZone(tzIdVienna); for (RDate rdate : list) rdate.setTimeZone(tz); assertEquals("20150101T103010Z,20150102T103020Z,20150103T103030Z,20150704T103040Z", DateUtils.recurrenceSetsToAndroidString(list, false)); // DATEs (without time) have to be converted to <date>T000000Z for Android list.clear(); list.add(new RDate(new DateList("20150101,20150702", Value.DATE))); assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true)); // DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to <date>T000000Z for Android list.clear(); list.add(new RDate(new DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME))); assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true)); } public void testAndroidStringToRecurrenceSets() throws ParseException { // list of UTC times ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, false); DateList exDates = exDate.getDates(); assertEquals(Value.DATE_TIME, exDates.getType()); assertTrue(exDates.isUtc()); assertEquals(2, exDates.size()); assertEquals(1420108210000L, exDates.get(0).getTime()); assertEquals(1435833020000L, exDates.get(1).getTime()); // list of time zone times exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(tzIdVienna + ";20150101T103010,20150702T103020", ExDate.class, false); exDates = exDate.getDates(); assertEquals(Value.DATE_TIME, exDates.getType()); assertEquals(DateUtils.tzRegistry.getTimeZone(tzIdVienna), exDates.getTimeZone()); assertEquals(2, exDates.size()); assertEquals(1420104610000L, exDates.get(0).getTime()); assertEquals(1435825820000L, exDates.get(1).getTime()); // list of dates exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, true); exDates = exDate.getDates(); assertEquals(Value.DATE, exDates.getType()); assertEquals(2, exDates.size()); assertEquals("20150101", exDates.get(0).toString()); assertEquals("20150702", exDates.get(1).toString()); } }
app/src/androidTest/java/at/bitfire/davdroid/resource/LocalCalendarTest.java +6 −21 Original line number Diff line number Diff line Loading @@ -30,17 +30,22 @@ 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; public class LocalCalendarTest extends InstrumentationTestCase { Loading Loading @@ -133,8 +138,7 @@ public class LocalCalendarTest extends InstrumentationTestCase { public void testBuildEntry() throws LocalStorageException, ParseException { final String vcardName = "testBuildEntry"; TimeZoneRegistry tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry(); TimeZone tzVienna = tzRegistry.getTimeZone("Europe/Vienna"); final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna"); assertNotNull(tzVienna); // build and write event to calendar provider Loading Loading @@ -231,23 +235,4 @@ public class LocalCalendarTest extends InstrumentationTestCase { } } public void testRecurrenceSetsToAndroidString() throws ParseException { final String tzId = "Europe/Vienna"; // one entry without time zone final List<RDate> list = new ArrayList<>(2); list.add(new RDate(new DateList("20150101T103000,20150102T103000", Value.DATE_TIME))); assertEquals("20150101T103000,20150102T103000", LocalCalendar.recurrenceSetsToAndroidString(list)); // two entries with time zone list.add(new RDate(new DateList("20150103T103000,20150104T103000", Value.DATE_TIME))); final TimeZoneRegistry tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry(); final TimeZone tz = tzRegistry.getTimeZone(tzId); for (RDate rdate : list) rdate.setTimeZone(tz); assertEquals(tzId + ";20150101T103000,20150102T103000,20150103T103000,20150104T103000", LocalCalendar.recurrenceSetsToAndroidString(list)); } }
app/src/main/java/at/bitfire/davdroid/DateUtils.java +114 −3 Original line number Diff line number Diff line Loading @@ -11,19 +11,43 @@ 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"; private final static TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry(); public final static 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 tzID) { String localTZ = null; Loading Loading @@ -56,8 +80,95 @@ public class DateUtils { return localTZ; } public static TimeZone getTimeZone(String tzID) { return tzRegistry.getTimeZone(tzID); // recurrence sets /** * Concatenates, if necessary, multiple RDATE/EXDATE lists and converts them to * a formatted string which Android calendar provider can process. * Android expects this format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss" (when * TZID is given) or "yyyymmddThhmmssZ". We don't use the TZID format here because then we're limited * to one time-zone, while an iCalendar may contain multiple EXDATE/RDATE lines with different time zones. * @param dates one more more lists of RDATE or EXDATE * @param allDay indicates whether the event is an all-day event or not * @return formatted string for Android calendar provider: * - in case of all-day events, all dates/times are returned as yyyymmddT000000Z * - in case of timed events, all dates/times are returned as UTC time: yyyymmddThhmmssZ */ public static String recurrenceSetsToAndroidString(List<? extends DateListProperty> dates, boolean allDay) throws ParseException { List<String> strDates = new LinkedList<>(); /* rdate/exdate: DATE DATE_TIME all-day store as ...T000000Z cut off time and store as ...T000000Z event with time (ignored) store as ...ThhmmssZ */ final DateFormat dateFormatUtcMidnight = new SimpleDateFormat("yyyyMMdd'T'000000'Z'"); for (DateListProperty dateListProp : dates) { final Value type = dateListProp.getDates().getType(); if (Value.DATE_TIME.equals(type)) { // DATE-TIME values will be stored in UTC format for Android if (allDay) { DateList dateList = dateListProp.getDates(); for (Date date : dateList) strDates.add(dateFormatUtcMidnight.format(date)); } else { dateListProp.setUtc(true); strDates.add(dateListProp.getValue()); } } else if (Value.DATE.equals(type)) // DATE values have to be converted to DATE-TIME <date>T000000Z for Android for (Date date : dateListProp.getDates()) strDates.add(dateFormatUtcMidnight.format(date)); } return StringUtils.join(strDates, ","); } /** * Takes a formatted string as provided by the Android calendar provider and returns a DateListProperty * constructed from these values. * @param dbStr formatted string from Android calendar provider (RDATE/EXDATE field) * expected format: "[TZID;]date1,date2,date3" where date is "yyyymmddThhmmss[Z]" * @param type subclass of DateListProperty, e.g. RDate or ExDate * @param allDay true: list will contain DATE values; false: list will contain DATE_TIME values * @return instance of "type" containing the parsed dates/times from the string */ public static DateListProperty androidStringToRecurrenceSet(String dbStr, Class<? extends DateListProperty> type, boolean allDay) throws ParseException { // 1. split string into time zone and actual dates TimeZone timeZone; String datesStr; final int limiter = dbStr.indexOf(';'); if (limiter != -1) { // TZID given timeZone = DateUtils.tzRegistry.getTimeZone(dbStr.substring(0, limiter)); datesStr = dbStr.substring(limiter + 1); } else { timeZone = null; datesStr = dbStr; } // 2. process date string and generate list of DATEs or DATE-TIMEs DateList dateList; if (allDay) { dateList = new DateList(Value.DATE); for (String s: StringUtils.split(datesStr, ',')) dateList.add(new Date(new DateTime(s))); } else { dateList = new DateList(datesStr, Value.DATE_TIME, timeZone); if (timeZone == null) dateList.setUtc(true); } // 3. generate requested DateListProperty (RDate/ExDate) from list of DATEs or DATE-TIMEs DateListProperty list; try { list = (DateListProperty)type.getDeclaredConstructor(new Class[] { DateList.class } ).newInstance(dateList); if (dateList.getTimeZone() != null) list.setTimeZone(dateList.getTimeZone()); } catch (Exception e) { throw new ParseException("Couldn't create date/time list by reflection", -1); } return list; } }
app/src/main/java/at/bitfire/davdroid/resource/Event.java +2 −2 Original line number Diff line number Diff line Loading @@ -321,7 +321,7 @@ public class Event extends iCalendar { dtStart = new DtStart(new Date(tsStart)); } else { DateTime start = new DateTime(tsStart); start.setTimeZone(tzRegistry.getTimeZone(tzID)); start.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID)); dtStart = new DtStart(start); } } Loading @@ -340,7 +340,7 @@ public class Event extends iCalendar { dtEnd = new DtEnd(new Date(tsEnd)); } else { DateTime end = new DateTime(tsEnd); end.setTimeZone(tzRegistry.getTimeZone(tzID)); end.setTimeZone(DateUtils.tzRegistry.getTimeZone(tzID)); dtEnd = new DtEnd(end); } } Loading
app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java +17 −40 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ 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; Loading @@ -61,6 +62,7 @@ import java.net.URISyntaxException; import java.text.ParseException; import java.util.LinkedList; import java.util.List; import java.util.StringTokenizer; import at.bitfire.davdroid.DAVUtils; import at.bitfire.davdroid.DateUtils; Loading Loading @@ -349,8 +351,7 @@ public class LocalCalendar extends LocalCollection<Event> { String strRDate = values.getAsString(Events.RDATE); if (!StringUtils.isEmpty(strRDate)) { RDate rDate = new RDate(); rDate.setValue(strRDate); RDate rDate = (RDate)DateUtils.androidStringToRecurrenceSet(strRDate, RDate.class, allDay); e.getRdates().add(rDate); } Loading @@ -363,9 +364,7 @@ public class LocalCalendar extends LocalCollection<Event> { String strExDate = values.getAsString(Events.EXDATE); if (!StringUtils.isEmpty(strExDate)) { // always empty, see https://code.google.com/p/android/issues/detail?id=172411 ExDate exDate = new ExDate(); exDate.setValue(strExDate); ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(strExDate, ExDate.class, allDay); e.getExdates().add(exDate); } } catch (ParseException ex) { Loading Loading @@ -540,12 +539,20 @@ public class LocalCalendar extends LocalCollection<Event> { } if (!event.getRdates().isEmpty()) { recurring = true; builder.withValue(Events.RDATE, recurrenceSetsToAndroidString(event.getRdates())); 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.getExrule() != null) builder.withValue(Events.EXRULE, event.getExrule().getValue()); if (!event.getExdates().isEmpty()) builder.withValue(Events.EXDATE, recurrenceSetsToAndroidString(event.getExdates())); 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) Loading Loading @@ -731,34 +738,4 @@ public class LocalCalendar extends LocalCollection<Event> { return calendarsURI(account); } /** * Concatenates, if necessary, multiple RDATE/EXDATE lists and prepares * a formatted string as expected by Android calendar provider * @param dates one more more lists of RDATE or EXDATE * @return formatted string for Android calendar provider */ static String recurrenceSetsToAndroidString(List<? extends DateListProperty> dates) { String tzID = null; List<String> strDates = new LinkedList<>(); for (DateListProperty dateList : dates) { if (dateList.getTimeZone() != null) { String thisTzID = DateUtils.findAndroidTimezoneID(dateList.getTimeZone().getID()); if (tzID == null) tzID = thisTzID; else if (!tzID.equals(thisTzID)) Log.w(TAG, "Multiple EXDATEs/RDATEs with different time zones not supported by Android, using " + tzID + " for all dates"); } strDates.add(dateList.getValue()); } // Android expects this format: "[TZID;]date1,date2,date3" String dateStr = ""; if (tzID != null) dateStr += tzID + ";"; dateStr += StringUtils.join(strDates, ","); return dateStr; } }