From 243483a957a59e77861b324d7fc007f6abc6bb4d Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Tue, 28 Jul 2015 15:29:54 +0200 Subject: [PATCH] Improved iCal generation * move shared code to new iCalendar class * generate UIDs and file names with "_" instead of "@" to reduce encoding problems (closes #585) * tasks: validate "start date" and "completed at" time zones --- .../at/bitfire/davdroid/resource/Event.java | 87 ++----------- .../at/bitfire/davdroid/resource/Task.java | 26 ++-- .../bitfire/davdroid/resource/iCalendar.java | 123 ++++++++++++++++++ 3 files changed, 142 insertions(+), 94 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java 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 f01bed83..d5094c0b 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/Event.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/Event.java @@ -69,11 +69,9 @@ import lombok.NonNull; import lombok.Setter; -public class Event extends Resource { +public class Event extends iCalendar { private final static String TAG = "davdroid.Event"; - private final static TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry(); - @Getter @Setter protected RecurrenceId recurrenceId; @Getter @Setter protected String summary, location, description; @@ -97,15 +95,6 @@ public class Event extends Resource { @Getter protected List alarms = new LinkedList<>(); - static { - CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true); - CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true); - CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true); - - // disable automatic time-zone updates (causes unnecessary network traffic for most people) - System.setProperty("net.fortuna.ical4j.timezone.update.enabled", "false"); - } - public Event(String name, String ETag) { super(name, ETag); @@ -115,18 +104,6 @@ public class Event extends Resource { super(localID, name, ETag); } - - @Override - public void initialize() { - generateUID(); - name = uid.replace("@", "_") + ".ics"; - } - - protected void generateUID() { - UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid())); - uid = generator.generateUid().getValue(); - } - @Override @SuppressWarnings("unchecked") @@ -228,12 +205,6 @@ public class Event extends Resource { } - - @Override - public String getMimeType() { - return "text/calendar"; - } - @Override @SuppressWarnings("unchecked") public ByteArrayOutputStream toEntity() throws IOException { @@ -333,7 +304,13 @@ public class Event extends Resource { return event; } - + + // time helpers + + public boolean isAllDay() { + return !hasTime(dtStart); + } + public long getDtStartInMillis() { return dtStart.getDate().getTime(); } @@ -370,54 +347,6 @@ public class Event extends Resource { dtEnd = new DtEnd(end); } } - - - // helpers - - public boolean isAllDay() { - return !hasTime(dtStart); - } - protected static boolean hasTime(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 */ - protected static void validateTimeZone(DateProperty date) { - if (date.isUtc() || !hasTime(date)) - return; - - String tzID = getTzId(date); - if (tzID == null) - return; - - String localTZ = DateUtils.findAndroidTimezoneID(tzID); - date.setTimeZone(tzRegistry.getTimeZone(localTZ)); - } - - public static String TimezoneDefToTzId(String timezoneDef) throws IllegalArgumentException { - 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); - return timezone.getTimeZoneId().getValue(); - } - } catch (Exception ex) { - Log.w(TAG, "Can't understand time zone definition, ignoring", ex); - } - throw new IllegalArgumentException(); - } } 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 2b8ccaa5..044a4bd9 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/Task.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/Task.java @@ -49,7 +49,7 @@ import at.bitfire.davdroid.syncadapter.DavSyncAdapter; import lombok.Getter; import lombok.Setter; -public class Task extends Resource { +public class Task extends iCalendar { private final static String TAG = "davdroid.Task"; @Getter @Setter DateTime createdAt; @@ -76,13 +76,6 @@ public class Task extends Resource { super(localId, name, ETag); } - @Override - public void initialize() { - UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid())); - uid = generator.generateUid().getValue(); - name = uid + ".ics"; - } - @Override public void parseEntity(InputStream entity, AssetDownloader downloader) throws IOException, InvalidResourceException { @@ -104,6 +97,10 @@ public class Task extends Resource { if (todo.getUid() != null) uid = todo.getUid().getValue(); + else { + Log.w(TAG, "Received VTODO without UID, generating new one"); + generateUID(); + } if (todo.getCreated() != null) createdAt = todo.getCreated().getDateTime(); @@ -129,20 +126,19 @@ public class Task extends Resource { due = todo.getDue(); if (todo.getDuration() != null) duration = todo.getDuration(); - if (todo.getStartDate() != null) + if (todo.getStartDate() != null) { dtStart = todo.getStartDate(); - if (todo.getDateCompleted() != null) + validateTimeZone(dtStart); + } + if (todo.getDateCompleted() != null) { completedAt = todo.getDateCompleted(); + validateTimeZone(completedAt); + } if (todo.getPercentComplete() != null) percentComplete = todo.getPercentComplete().getPercentage(); } - @Override - public String getMimeType() { - return "text/calendar"; - } - @Override public ByteArrayOutputStream toEntity() throws IOException { final net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar(); diff --git a/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java new file mode 100644 index 00000000..daac0fa2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/iCalendar.java @@ -0,0 +1,123 @@ +/* + * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.bitfire.davdroid.resource; + +import android.text.format.Time; +import android.util.Log; + +import net.fortuna.ical4j.data.CalendarBuilder; +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 at.bitfire.davdroid.DateUtils; +import at.bitfire.davdroid.syncadapter.DavSyncAdapter; +import lombok.Getter; + +public abstract class iCalendar extends Resource { + static private final String TAG = "DAVdroid.iCal"; + + // static ical4j initialization + static { + CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true); + CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true); + CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true); + + // disable automatic time-zone updates (causes unwanted network traffic) + System.setProperty("net.fortuna.ical4j.timezone.update.enabled", "false"); + } + + static protected final TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry(); + + + public iCalendar(long localID, String name, String ETag) { + super(localID, name, ETag); + } + + public iCalendar(String name, String ETag) { + super(name, ETag); + } + + + @Override + public void initialize() { + generateUID(); + name = uid + ".ics"; + } + + protected void generateUID() { + UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid())); + uid = generator.generateUid().getValue().replace("@", "_"); + } + + + @Override + public String getMimeType() { + return "text/calendar"; + } + + + // time zone helpers + + protected static boolean hasTime(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 */ + protected static void validateTimeZone(DateProperty date) { + if (date.isUtc() || !hasTime(date)) + return; + + String tzID = getTzId(date); + if (tzID == null) + return; + + String localTZ = DateUtils.findAndroidTimezoneID(tzID); + date.setTimeZone(tzRegistry.getTimeZone(localTZ)); + } + + public static String TimezoneDefToTzId(String timezoneDef) throws IllegalArgumentException { + 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); + return timezone.getTimeZoneId().getValue(); + } + } catch (Exception ex) { + Log.w(TAG, "Can't understand time zone definition, ignoring", ex); + } + throw new IllegalArgumentException(); + } + +}