1
0
mirror of https://github.com/etesync/android synced 2024-11-26 09:58:11 +00:00

support for reminders; better support for recurring events; bug fixes

* use DURATION instead of DTEND when required (fixes #60)
* send VTIMEZONEs along with VEVENTs (fixes #66)
* support for reminders (closes #70)
* bug fixes (UID parsing)
This commit is contained in:
rfc2822 2013-11-24 13:15:31 +01:00
parent 55d9dc4357
commit 759e32b89b
7 changed files with 108 additions and 30 deletions

View File

@ -30,6 +30,7 @@ import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList; import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.TimeZoneRegistry; import net.fortuna.ical4j.model.TimeZoneRegistry;
import net.fortuna.ical4j.model.ValidationException; 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.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone; import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.parameter.Value; 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.Description;
import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtEnd;
import net.fortuna.ical4j.model.property.DtStart; 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.ExDate;
import net.fortuna.ical4j.model.property.ExRule; import net.fortuna.ical4j.model.property.ExRule;
import net.fortuna.ical4j.model.property.Location; import net.fortuna.ical4j.model.property.Location;
@ -65,6 +67,7 @@ public class Event extends Resource {
@Getter private DtStart dtStart; @Getter private DtStart dtStart;
@Getter private DtEnd dtEnd; @Getter private DtEnd dtEnd;
@Getter @Setter private Duration duration;
@Getter @Setter private RDate rdate; @Getter @Setter private RDate rdate;
@Getter @Setter private RRule rrule; @Getter @Setter private RRule rrule;
@Getter @Setter private ExDate exdate; @Getter @Setter private ExDate exdate;
@ -80,6 +83,11 @@ public class Event extends Resource {
attendees.add(attendee); attendees.add(attendee);
} }
@Getter private List<VAlarm> alarms = new LinkedList<VAlarm>();
public void addAlarm(VAlarm alarm) {
alarms.add(alarm);
}
public Event(String name, String ETag) { public Event(String name, String ETag) {
super(name, ETag); super(name, ETag);
@ -95,12 +103,15 @@ public class Event extends Resource {
@Override @Override
@SuppressWarnings("unchecked")
public void parseEntity(@NonNull InputStream entity) throws IOException, ParserException { public void parseEntity(@NonNull InputStream entity) throws IOException, ParserException {
CalendarBuilder builder = new CalendarBuilder(); CalendarBuilder builder = new CalendarBuilder();
net.fortuna.ical4j.model.Calendar ical = builder.build(entity); net.fortuna.ical4j.model.Calendar ical = builder.build(entity);
if (ical == null) if (ical == null)
return; return;
Log.d(TAG, "Parsing iCal: " + ical.toString());
// event // event
ComponentList events = ical.getComponents(Component.VEVENT); ComponentList events = ical.getComponents(Component.VEVENT);
if (events == null || events.isEmpty()) if (events == null || events.isEmpty())
@ -108,7 +119,7 @@ public class Event extends Resource {
VEvent event = (VEvent)events.get(0); VEvent event = (VEvent)events.get(0);
if (event.getUid() != null) if (event.getUid() != null)
uid = event.getUid().toString(); uid = event.getUid().getValue();
else { else {
Log.w(TAG, "Received VEVENT without UID, generating new one"); Log.w(TAG, "Received VEVENT without UID, generating new one");
UidGenerator uidGenerator = new UidGenerator(Integer.toString(android.os.Process.myPid())); UidGenerator uidGenerator = new UidGenerator(Integer.toString(android.os.Process.myPid()));
@ -118,6 +129,7 @@ public class Event extends Resource {
dtStart = event.getStartDate(); validateTimeZone(dtStart); dtStart = event.getStartDate(); validateTimeZone(dtStart);
dtEnd = event.getEndDate(); validateTimeZone(dtEnd); dtEnd = event.getEndDate(); validateTimeZone(dtEnd);
duration = event.getDuration();
rrule = (RRule)event.getProperty(Property.RRULE); rrule = (RRule)event.getProperty(Property.RRULE);
rdate = (RDate)event.getProperty(Property.RDATE); rdate = (RDate)event.getProperty(Property.RDATE);
exrule = (ExRule)event.getProperty(Property.EXRULE); exrule = (ExRule)event.getProperty(Property.EXRULE);
@ -144,10 +156,11 @@ public class Event extends Resource {
forPublic = false; forPublic = false;
} }
Log.i(TAG, "Parsed iCal: " + ical.toString()); this.alarms = event.getAlarms();
} }
@Override @Override
@SuppressWarnings("unchecked")
public String toEntity() { public String toEntity() {
net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar(); net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar();
ical.getProperties().add(Version.VERSION_2_0); ical.getProperties().add(Version.VERSION_2_0);
@ -160,7 +173,10 @@ public class Event extends Resource {
props.add(new Uid(uid)); props.add(new Uid(uid));
props.add(dtStart); props.add(dtStart);
if (dtEnd != null)
props.add(dtEnd); props.add(dtEnd);
if (duration != null)
props.add(duration);
if (rrule != null) if (rrule != null)
props.add(rrule); props.add(rrule);
@ -183,18 +199,23 @@ public class Event extends Resource {
if (organizer != null) if (organizer != null)
props.add(organizer); props.add(organizer);
for (Attendee attendee : attendees) props.addAll(attendees);
props.add(attendee);
if (forPublic != null) if (forPublic != null)
event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE); event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
event.getAlarms().addAll(alarms);
ical.getComponents().add(event); ical.getComponents().add(event);
/*if (dtStart.getTimeZone() != null) // add VTIMEZONE components
ical.getComponents().add(dtStart.getTimeZone().getVTimeZone()); net.fortuna.ical4j.model.TimeZone
if (dtEnd.getTimeZone() != null) tzStart = (dtStart == null ? null : dtStart.getTimeZone()),
ical.getComponents().add(dtEnd.getTimeZone().getVTimeZone());*/ 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(); return ical.toString();
} }
@ -280,7 +301,7 @@ public class Event extends Resource {
/* guess matching Android timezone ID */ /* guess matching Android timezone ID */
protected void validateTimeZone(DateProperty date) { protected void validateTimeZone(DateProperty date) {
if (date.isUtc() || hasNoTime(date)) if (date == null || date.isUtc() || hasNoTime(date))
return; return;
String tzID = getTzId(date); String tzID = getTzId(date);

View File

@ -15,13 +15,19 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import lombok.Getter; import lombok.Getter;
import net.fortuna.ical4j.model.Dur;
import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.ParameterList; 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.Cn;
import net.fortuna.ical4j.model.parameter.CuType; import net.fortuna.ical4j.model.parameter.CuType;
import net.fortuna.ical4j.model.parameter.PartStat; import net.fortuna.ical4j.model.parameter.PartStat;
import net.fortuna.ical4j.model.parameter.Role; 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.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.ExDate;
import net.fortuna.ical4j.model.property.ExRule; import net.fortuna.ical4j.model.property.ExRule;
import net.fortuna.ical4j.model.property.Organizer; import net.fortuna.ical4j.model.property.Organizer;
@ -49,6 +55,7 @@ import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.util.Log; import android.util.Log;
import at.bitfire.davdroid.syncadapter.ServerInfo; import at.bitfire.davdroid.syncadapter.ServerInfo;
@ -188,7 +195,7 @@ public class LocalCalendar extends LocalCollection<Event> {
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL, /* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE, /* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS, /* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
/* 17 */ entryColumnUID() /* 17 */ entryColumnUID(), Events.DURATION
}, null, null, null); }, null, null, null);
if (cursor != null && cursor.moveToNext()) { if (cursor != null && cursor.moveToNext()) {
e.setUid(cursor.getString(17)); e.setUid(cursor.getString(17));
@ -199,23 +206,27 @@ public class LocalCalendar extends LocalCollection<Event> {
long tsStart = cursor.getLong(3), long tsStart = cursor.getLong(3),
tsEnd = cursor.getLong(4); tsEnd = cursor.getLong(4);
if (cursor.getInt(7) != 0) { // all-day, UTC
e.setDtStart(tsStart, null); String tzId;
e.setDtEnd(tsEnd, null); if (cursor.getInt(7) != 0) { // ALL_DAY != 0
tzId = null; // -> use UTC
} else { } else {
// use the start time zone for the end time, too // use the start time zone for the end time, too
// because the Samsung Planner UI allows the user to change the time zone // because the Samsung Planner UI allows the user to change the time zone
// but it will change the start time zone only // but it will change the start time zone only
tzId = cursor.getString(5);
String tzIdStart = cursor.getString(5);
//tzIdEnd = cursor.getString(6); //tzIdEnd = cursor.getString(6);
e.setDtStart(tsStart, tzIdStart);
e.setDtEnd(tsEnd, tzIdStart /*(tzIdEnd != null) ? tzIdEnd : tzIdStart*/);
} }
e.setDtStart(tsStart, tzId);
if (tsEnd != 0)
e.setDtEnd(tsEnd, tzId);
// recurrence // recurrence
try { try {
String duration = cursor.getString(18);
if (duration != null)
e.setDuration(new Duration(new Dur(duration)));
String strRRule = cursor.getString(10); String strRRule = cursor.getString(10);
if (strRRule != null) if (strRRule != null)
e.setRrule(new RRule(strRRule)); e.setRrule(new RRule(strRRule));
@ -339,6 +350,28 @@ public class LocalCalendar extends LocalCollection<Event> {
case Events.ACCESS_PUBLIC: case Events.ACCESS_PUBLIC:
e.setForPublic(true); 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<Event> {
protected void addDataRows(Event event, long localID, int backrefIdx) { protected void addDataRows(Event event, long localID, int backrefIdx) {
for (Attendee attendee : event.getAttendees()) for (Attendee attendee : event.getAttendees())
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build()); 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 @Override
@ -443,6 +478,9 @@ public class LocalCalendar extends LocalCollection<Event> {
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI)) pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
.withSelection(Attendees.EVENT_ID + "=?", .withSelection(Attendees.EVENT_ID + "=?",
new String[] { String.valueOf(event.getLocalID()) }).build()); 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<Event> {
.withValue(Attendees.ATTENDEE_TYPE, type) .withValue(Attendees.ATTENDEE_TYPE, type)
.withValue(Attendees.ATTENDEE_STATUS, status); .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);
}
} }

View File

@ -7,6 +7,7 @@ import java.net.UnknownHostException;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocket;
import org.apache.http.conn.scheme.LayeredSocketFactory; import org.apache.http.conn.scheme.LayeredSocketFactory;
@ -66,9 +67,13 @@ public class TlsSniSocketFactory implements LayeredSocketFactory {
Log.i(TAG, "No SNI support below Android 4.2!"); Log.i(TAG, "No SNI support below Android 4.2!");
// verify hostname and certificate // 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); throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
" using " + session.getCipherSuite());
return ssl; return ssl;
} }
} }

View File

@ -96,13 +96,13 @@ public class WebDavResource {
if (trailingSlash && !location.getRawPath().endsWith("/")) if (trailingSlash && !location.getRawPath().endsWith("/"))
location = new URI(location.getScheme(), location.getSchemeSpecificPart() + "/", null); 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 { public WebDavResource(URI baseURL, String username, String password, boolean preemptive, boolean trailingSlash) throws URISyntaxException {
this(baseURL, trailingSlash); this(baseURL, trailingSlash);
client = DavHttpClient.getDefault();
// authenticate // authenticate
client.getCredentialsProvider().setCredentials( client.getCredentialsProvider().setCredentials(
new AuthScope(location.getHost(), location.getPort()), new AuthScope(location.getHost(), location.getPort()),
@ -350,6 +350,7 @@ public class WebDavResource {
public void put(byte[] data, PutMode mode) throws IOException, HttpException { public void put(byte[] data, PutMode mode) throws IOException, HttpException {
HttpPut put = new HttpPut(location); HttpPut put = new HttpPut(location);
Log.d(TAG, "Sending PUT request: " + new String(data, "UTF-8"));
put.setEntity(new ByteArrayEntity(data)); put.setEntity(new ByteArrayEntity(data));
switch (mode) { switch (mode) {

View File

@ -29,7 +29,7 @@ public class CalendarTest extends InstrumentationTestCase {
Assert.assertEquals("Test-Ereignis im schönen Wien", e.getSummary()); Assert.assertEquals("Test-Ereignis im schönen Wien", e.getSummary());
//DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000 //DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000
//Assert.assertEquals(1381327200, e.getDtStartInMillis()); Assert.assertEquals(1381327200, e.getDtStartInMillis());
} }

View File

@ -1,7 +1,6 @@
package at.bitfire.davdroid.test; package at.bitfire.davdroid.test;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.URIUtils; import at.bitfire.davdroid.URIUtils;

View File

@ -21,7 +21,7 @@ import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
// tests require running robohydra! // tests require running robohydra!
public class WebDavResourceTest extends InstrumentationTestCase { 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 }; static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
AssetManager assetMgr; AssetManager assetMgr;