diff --git a/src/at/bitfire/davdroid/resource/Event.java b/src/at/bitfire/davdroid/resource/Event.java index 8145550a..526cc214 100644 --- a/src/at/bitfire/davdroid/resource/Event.java +++ b/src/at/bitfire/davdroid/resource/Event.java @@ -30,6 +30,7 @@ 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; @@ -39,6 +40,7 @@ import net.fortuna.ical4j.model.property.DateProperty; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStart; +import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.ExDate; import net.fortuna.ical4j.model.property.ExRule; import net.fortuna.ical4j.model.property.Location; @@ -65,6 +67,7 @@ public class Event extends Resource { @Getter private DtStart dtStart; @Getter private DtEnd dtEnd; + @Getter @Setter private Duration duration; @Getter @Setter private RDate rdate; @Getter @Setter private RRule rrule; @Getter @Setter private ExDate exdate; @@ -80,6 +83,11 @@ public class Event extends Resource { attendees.add(attendee); } + @Getter private List alarms = new LinkedList(); + public void addAlarm(VAlarm alarm) { + alarms.add(alarm); + } + public Event(String name, String ETag) { super(name, ETag); @@ -95,12 +103,15 @@ public class Event extends Resource { @Override + @SuppressWarnings("unchecked") public void parseEntity(@NonNull InputStream entity) throws IOException, ParserException { CalendarBuilder builder = new CalendarBuilder(); net.fortuna.ical4j.model.Calendar ical = builder.build(entity); if (ical == null) return; + Log.d(TAG, "Parsing iCal: " + ical.toString()); + // event ComponentList events = ical.getComponents(Component.VEVENT); if (events == null || events.isEmpty()) @@ -108,7 +119,7 @@ public class Event extends Resource { VEvent event = (VEvent)events.get(0); if (event.getUid() != null) - uid = event.getUid().toString(); + uid = event.getUid().getValue(); else { Log.w(TAG, "Received VEVENT without UID, generating new one"); UidGenerator uidGenerator = new UidGenerator(Integer.toString(android.os.Process.myPid())); @@ -116,8 +127,9 @@ public class Event extends Resource { } dtStart = event.getStartDate(); validateTimeZone(dtStart); - dtEnd = event.getEndDate(); validateTimeZone(dtEnd); + dtEnd = event.getEndDate(); validateTimeZone(dtEnd); + duration = event.getDuration(); rrule = (RRule)event.getProperty(Property.RRULE); rdate = (RDate)event.getProperty(Property.RDATE); exrule = (ExRule)event.getProperty(Property.EXRULE); @@ -144,10 +156,11 @@ public class Event extends Resource { forPublic = false; } - Log.i(TAG, "Parsed iCal: " + ical.toString()); + this.alarms = event.getAlarms(); } @Override + @SuppressWarnings("unchecked") public String toEntity() { net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar(); ical.getProperties().add(Version.VERSION_2_0); @@ -160,7 +173,10 @@ public class Event extends Resource { props.add(new Uid(uid)); props.add(dtStart); - props.add(dtEnd); + if (dtEnd != null) + props.add(dtEnd); + if (duration != null) + props.add(duration); if (rrule != null) props.add(rrule); @@ -183,18 +199,23 @@ public class Event extends Resource { if (organizer != null) props.add(organizer); - for (Attendee attendee : attendees) - props.add(attendee); + props.addAll(attendees); if (forPublic != null) event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE); - - ical.getComponents().add(event); - /*if (dtStart.getTimeZone() != null) - ical.getComponents().add(dtStart.getTimeZone().getVTimeZone()); - if (dtEnd.getTimeZone() != null) - ical.getComponents().add(dtEnd.getTimeZone().getVTimeZone());*/ + event.getAlarms().addAll(alarms); + + ical.getComponents().add(event); + + // add VTIMEZONE components + net.fortuna.ical4j.model.TimeZone + tzStart = (dtStart == null ? null : dtStart.getTimeZone()), + tzEnd = (dtEnd == null ? null : dtEnd.getTimeZone()); + if (tzStart != null) + ical.getComponents().add(tzStart.getVTimeZone()); + if (tzEnd != null && tzEnd != tzStart) + ical.getComponents().add(tzEnd.getVTimeZone()); return ical.toString(); } @@ -280,7 +301,7 @@ public class Event extends Resource { /* guess matching Android timezone ID */ protected void validateTimeZone(DateProperty date) { - if (date.isUtc() || hasNoTime(date)) + if (date == null || date.isUtc() || hasNoTime(date)) return; String tzID = getTzId(date); diff --git a/src/at/bitfire/davdroid/resource/LocalCalendar.java b/src/at/bitfire/davdroid/resource/LocalCalendar.java index cadd2a42..729e6af6 100644 --- a/src/at/bitfire/davdroid/resource/LocalCalendar.java +++ b/src/at/bitfire/davdroid/resource/LocalCalendar.java @@ -15,13 +15,19 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.Getter; +import net.fortuna.ical4j.model.Dur; import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.ParameterList; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.component.VAlarm; import net.fortuna.ical4j.model.parameter.Cn; import net.fortuna.ical4j.model.parameter.CuType; import net.fortuna.ical4j.model.parameter.PartStat; import net.fortuna.ical4j.model.parameter.Role; +import net.fortuna.ical4j.model.property.Action; import net.fortuna.ical4j.model.property.Attendee; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.ExDate; import net.fortuna.ical4j.model.property.ExRule; import net.fortuna.ical4j.model.property.Organizer; @@ -49,6 +55,7 @@ import android.provider.CalendarContract; import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; +import android.provider.CalendarContract.Reminders; import android.provider.ContactsContract; import android.util.Log; import at.bitfire.davdroid.syncadapter.ServerInfo; @@ -188,7 +195,7 @@ public class LocalCalendar extends LocalCollection { /* 8 */ Events.STATUS, Events.ACCESS_LEVEL, /* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE, /* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS, - /* 17 */ entryColumnUID() + /* 17 */ entryColumnUID(), Events.DURATION }, null, null, null); if (cursor != null && cursor.moveToNext()) { e.setUid(cursor.getString(17)); @@ -199,23 +206,27 @@ public class LocalCalendar extends LocalCollection { long tsStart = cursor.getLong(3), tsEnd = cursor.getLong(4); - if (cursor.getInt(7) != 0) { // all-day, UTC - e.setDtStart(tsStart, null); - e.setDtEnd(tsEnd, null); + + String tzId; + if (cursor.getInt(7) != 0) { // ALL_DAY != 0 + tzId = null; // -> use UTC } else { // use the start time zone for the end time, too // because the Samsung Planner UI allows the user to change the time zone // but it will change the start time zone only - - String tzIdStart = cursor.getString(5); - //tzIdEnd = cursor.getString(6); - - e.setDtStart(tsStart, tzIdStart); - e.setDtEnd(tsEnd, tzIdStart /*(tzIdEnd != null) ? tzIdEnd : tzIdStart*/); + tzId = cursor.getString(5); + //tzIdEnd = cursor.getString(6); } - + e.setDtStart(tsStart, tzId); + if (tsEnd != 0) + e.setDtEnd(tsEnd, tzId); + // recurrence try { + String duration = cursor.getString(18); + if (duration != null) + e.setDuration(new Duration(new Dur(duration))); + String strRRule = cursor.getString(10); if (strRRule != null) e.setRrule(new RRule(strRRule)); @@ -339,6 +350,28 @@ public class LocalCalendar extends LocalCollection { case Events.ACCESS_PUBLIC: e.setForPublic(true); } + + // reminders + Uri remindersUri = Reminders.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + Cursor c = providerClient.query(remindersUri, new String[] { + /* 0 */ Reminders.MINUTES, Reminders.METHOD + }, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null); + while (c != null && c.moveToNext()) { + VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0)); + + PropertyList props = alarm.getProperties(); + switch (c.getInt(1)) { + /*case Reminders.METHOD_EMAIL: + props.add(Action.EMAIL); + break;*/ + default: + props.add(Action.DISPLAY); + props.add(new Description(e.getSummary())); + } + e.addAlarm(alarm); + } } } @@ -436,6 +469,8 @@ public class LocalCalendar extends LocalCollection { protected void addDataRows(Event event, long localID, int backrefIdx) { for (Attendee attendee : event.getAttendees()) pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build()); + for (VAlarm alarm : event.getAlarms()) + pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build()); } @Override @@ -443,6 +478,9 @@ public class LocalCalendar extends LocalCollection { pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI)) .withSelection(Attendees.EVENT_ID + "=?", new String[] { String.valueOf(event.getLocalID()) }).build()); + pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI)) + .withSelection(Reminders.EVENT_ID + "=?", + new String[] { String.valueOf(event.getLocalID()) }).build()); } @@ -491,4 +529,18 @@ public class LocalCalendar extends LocalCollection { .withValue(Attendees.ATTENDEE_TYPE, type) .withValue(Attendees.ATTENDEE_STATUS, status); } + + protected Builder buildReminder(Builder builder, VAlarm alarm) { + int minutes = 0; + + Dur duration; + if (alarm.getTrigger() != null && (duration = alarm.getTrigger().getDuration()) != null) + minutes = duration.getDays() * 24*60 + duration.getHours()*60 + duration.getMinutes(); + + Log.i(TAG, "Adding alarm " + minutes + " min before"); + + return builder + .withValue(Reminders.METHOD, Reminders.METHOD_ALERT) + .withValue(Reminders.MINUTES, minutes); + } } diff --git a/src/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java b/src/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java index 4ed40bf6..2216f069 100644 --- a/src/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java +++ b/src/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java @@ -7,6 +7,7 @@ import java.net.UnknownHostException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import org.apache.http.conn.scheme.LayeredSocketFactory; @@ -66,8 +67,12 @@ public class TlsSniSocketFactory implements LayeredSocketFactory { Log.i(TAG, "No SNI support below Android 4.2!"); // verify hostname and certificate - if (!hostnameVerifier.verify(host, ssl.getSession())) + SSLSession session = ssl.getSession(); + if (!hostnameVerifier.verify(host, session)) throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host); + + Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + + " using " + session.getCipherSuite()); return ssl; } diff --git a/src/at/bitfire/davdroid/webdav/WebDavResource.java b/src/at/bitfire/davdroid/webdav/WebDavResource.java index 9568734f..3504c31b 100644 --- a/src/at/bitfire/davdroid/webdav/WebDavResource.java +++ b/src/at/bitfire/davdroid/webdav/WebDavResource.java @@ -96,13 +96,13 @@ public class WebDavResource { if (trailingSlash && !location.getRawPath().endsWith("/")) location = new URI(location.getScheme(), location.getSchemeSpecificPart() + "/", null); + + client = DavHttpClient.getDefault(); } public WebDavResource(URI baseURL, String username, String password, boolean preemptive, boolean trailingSlash) throws URISyntaxException { this(baseURL, trailingSlash); - client = DavHttpClient.getDefault(); - // authenticate client.getCredentialsProvider().setCredentials( new AuthScope(location.getHost(), location.getPort()), @@ -350,6 +350,7 @@ public class WebDavResource { public void put(byte[] data, PutMode mode) throws IOException, HttpException { HttpPut put = new HttpPut(location); + Log.d(TAG, "Sending PUT request: " + new String(data, "UTF-8")); put.setEntity(new ByteArrayEntity(data)); switch (mode) { diff --git a/test/src/at/bitfire/davdroid/test/CalendarTest.java b/test/src/at/bitfire/davdroid/test/CalendarTest.java index f0ff74b0..cf2828c9 100644 --- a/test/src/at/bitfire/davdroid/test/CalendarTest.java +++ b/test/src/at/bitfire/davdroid/test/CalendarTest.java @@ -29,7 +29,7 @@ public class CalendarTest extends InstrumentationTestCase { Assert.assertEquals("Test-Ereignis im schönen Wien", e.getSummary()); //DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000 - //Assert.assertEquals(1381327200, e.getDtStartInMillis()); + Assert.assertEquals(1381327200, e.getDtStartInMillis()); } diff --git a/test/src/at/bitfire/davdroid/test/URIUtilsTest.java b/test/src/at/bitfire/davdroid/test/URIUtilsTest.java index 549643b2..28ceae0b 100644 --- a/test/src/at/bitfire/davdroid/test/URIUtilsTest.java +++ b/test/src/at/bitfire/davdroid/test/URIUtilsTest.java @@ -1,7 +1,6 @@ package at.bitfire.davdroid.test; import java.net.URI; -import java.net.URISyntaxException; import android.test.InstrumentationTestCase; import at.bitfire.davdroid.URIUtils; diff --git a/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java b/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java index 322c61e6..42610ad5 100644 --- a/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java +++ b/test/src/at/bitfire/davdroid/webdav/test/WebDavResourceTest.java @@ -21,7 +21,7 @@ import at.bitfire.davdroid.webdav.WebDavResource.PutMode; // tests require running robohydra! public class WebDavResourceTest extends InstrumentationTestCase { - static final String ROBOHYDRA_BASE = "http://10.0.0.119:3000/"; + static final String ROBOHYDRA_BASE = "http://10.0.0.11:3000/"; static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 }; AssetManager assetMgr;