From 1c48450c02ea33e498e17a1bfc9f95576d3027b6 Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Wed, 31 Mar 2010 19:24:18 -0700 Subject: [PATCH] Fix the VCALENDAR we send with all day events * We need to send date only (without time) in the VCALENDAR file for all-day events * Add unit test for this case Bug: 2561789 Change-Id: I33a43c7a248059c97482ca147a23af083744118a --- .../exchange/utility/CalendarUtilities.java | 62 +++++++++------ .../utility/CalendarUtilitiesTests.java | 76 ++++++++++++++++++- 2 files changed, 114 insertions(+), 24 deletions(-) diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java index 8a97b9d27..1c2ddce30 100644 --- a/src/com/android/exchange/utility/CalendarUtilities.java +++ b/src/com/android/exchange/utility/CalendarUtilities.java @@ -795,28 +795,32 @@ public class CalendarUtilities { * Generate an EAS formatted date/time string based on GMT. See below for details. */ static public String millisToEasDateTime(long millis) { - return millisToEasDateTime(millis, sGmtTimeZone); + return millisToEasDateTime(millis, sGmtTimeZone, true); } /** - * Generate an EAS formatted local date/time string from a time and a time zone + * Generate an EAS formatted local date/time string from a time and a time zone. If the final + * argument is false, only a date will be returned (e.g. 20100331) * @param millis a time in milliseconds * @param tz a time zone - * @return an EAS formatted string indicating the date/time in the given time zone + * @param withTime if the time is to be included in the string + * @return an EAS formatted string indicating the date (and time) in the given time zone */ - static public String millisToEasDateTime(long millis, TimeZone tz) { + static public String millisToEasDateTime(long millis, TimeZone tz, boolean withTime) { StringBuilder sb = new StringBuilder(); GregorianCalendar cal = new GregorianCalendar(tz); cal.setTimeInMillis(millis); sb.append(cal.get(Calendar.YEAR)); sb.append(formatTwo(cal.get(Calendar.MONTH) + 1)); sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH))); - sb.append('T'); - sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY))); - sb.append(formatTwo(cal.get(Calendar.MINUTE))); - sb.append(formatTwo(cal.get(Calendar.SECOND))); - if (tz == sGmtTimeZone) { - sb.append('Z'); + if (withTime) { + sb.append('T'); + sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY))); + sb.append(formatTwo(cal.get(Calendar.MINUTE))); + sb.append(formatTwo(cal.get(Calendar.SECOND))); + if (tz == sGmtTimeZone) { + sb.append('Z'); + } } return sb.toString(); } @@ -1245,15 +1249,28 @@ public class CalendarUtilities { // Our default vcalendar time zone is UTC, but this will change (below) if we're // sending a recurring event, in which case we use local time TimeZone vCalendarTimeZone = sGmtTimeZone; - String vCalendarTimeZoneSuffix = ""; + String vCalendarDateSuffix = ""; + + // Check for all day event + boolean allDayEvent = false; + if (entityValues.containsKey(Events.ALL_DAY)) { + Integer ade = entityValues.getAsInteger(Events.ALL_DAY); + allDayEvent = (ade != null) && (ade == 1); + if (allDayEvent) { + // Example: DTSTART;VALUE=DATE:20100331 (all day event) + vCalendarDateSuffix = ";VALUE=DATE"; + } + } // If we're inviting people and the meeting is recurring, we need to send our time zone - // information and make sure to send DTSTART/DTEND in local time - if (!isReply && entityValues.containsKey(Events.RRULE)) { + // information and make sure to send DTSTART/DTEND in local time (unless, of course, + // this is an all-day event) + if (!isReply && entityValues.containsKey(Events.RRULE) && !allDayEvent) { vCalendarTimeZone = TimeZone.getDefault(); // Write the VTIMEZONE block to the writer timeZoneToVTimezone(vCalendarTimeZone, ics); - vCalendarTimeZoneSuffix = ";TZID=" + vCalendarTimeZone.getID(); + // Example: DTSTART;TZID=US/Pacific:20100331T124500 + vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID(); } ics.writeTag("BEGIN", "VEVENT"); @@ -1272,23 +1289,24 @@ public class CalendarUtilities { long startTime = entityValues.getAsLong(Events.DTSTART); if (startTime != 0) { - ics.writeTag("DTSTART" + vCalendarTimeZoneSuffix, - millisToEasDateTime(startTime, vCalendarTimeZone)); + ics.writeTag("DTSTART" + vCalendarDateSuffix, + millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent)); } // If this is an Exception, we send the recurrence-id, which is just the original // instance time if (isException) { long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME); - ics.writeTag("RECURRENCE-ID" + vCalendarTimeZoneSuffix, - millisToEasDateTime(originalTime, vCalendarTimeZone)); + ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix, + millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent)); } if (!entityValues.containsKey(Events.DURATION)) { if (entityValues.containsKey(Events.DTEND)) { - ics.writeTag("DTEND" + vCalendarTimeZoneSuffix, + ics.writeTag("DTEND" + vCalendarDateSuffix, millisToEasDateTime( - entityValues.getAsLong(Events.DTEND), vCalendarTimeZone)); + entityValues.getAsLong(Events.DTEND), vCalendarTimeZone, + !allDayEvent)); } } else { // Convert this into millis and add it to DTSTART for DTEND @@ -1300,9 +1318,9 @@ public class CalendarUtilities { } catch (ParseException e) { // We'll use the default in this case } - ics.writeTag("DTEND" + vCalendarTimeZoneSuffix, + ics.writeTag("DTEND" + vCalendarDateSuffix, millisToEasDateTime( - startTime + durationMillis, vCalendarTimeZone)); + startTime + durationMillis, vCalendarTimeZone, !allDayEvent)); } String location = null; diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java index 58dfd2371..0c6c39732 100644 --- a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java +++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java @@ -265,6 +265,70 @@ public class CalendarUtilitiesTests extends AndroidTestCase { //TODO Check the contents of the attachment using an iCalendar parser } + public void testCreateMessageForEntity_Invite_AllDay() throws IOException { + // Set up the "event" + String title = "Discuss Unit Tests"; + Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title); + entity.getEntityValues().put(Events.ALL_DAY, 1); + + // Create a dummy account for the attendee + Account account = new Account(); + account.mEmailAddress = ORGANIZER; + + // The uid is required, but can be anything + String uid = "31415926535"; + + // Create the outgoing message + Message msg = CalendarUtilities.createMessageForEntity(mContext, entity, + Message.FLAG_OUTGOING_MEETING_INVITE, uid, account); + + // First, we should have a message + assertNotNull(msg); + + // Now check some of the fields of the message + assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo); + String accept = getContext().getResources().getString(R.string.meeting_invitation, title); + assertEquals(accept, msg.mSubject); + + // And make sure we have an attachment + assertNotNull(msg.mAttachments); + assertEquals(1, msg.mAttachments.size()); + Attachment att = msg.mAttachments.get(0); + // And that the attachment has the correct elements + assertEquals("invite.ics", att.mFileName); + assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART, + att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART); + assertEquals("text/calendar; method=REQUEST", att.mMimeType); + assertNotNull(att.mContentBytes); + assertEquals(att.mSize, att.mContentBytes.length); + + // We'll check the contents of the ics file here + BlockHash vcalendar = parseIcsContent(att.mContentBytes); + assertNotNull(vcalendar); + + // We should have a VCALENDAR with a REQUEST method + assertEquals("VCALENDAR", vcalendar.name); + assertEquals("REQUEST", vcalendar.get("METHOD")); + + // We should have one block under VCALENDAR + assertEquals(1, vcalendar.blocks.size()); + BlockHash vevent = vcalendar.blocks.get(0); + // It's a VEVENT with the following fields + assertEquals("VEVENT", vevent.name); + assertEquals("Meeting Location", vevent.get("LOCATION")); + assertEquals("0", vevent.get("SEQUENCE")); + assertEquals("Discuss Unit Tests", vevent.get("SUMMARY")); + assertEquals(uid, vevent.get("UID")); + assertEquals("MAILTO:" + ATTENDEE, + vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE")); + + // These next two fields should have a date only + assertEquals("20100412", vevent.get("DTSTART;VALUE=DATE")); + assertEquals("20100412", vevent.get("DTEND;VALUE=DATE")); + // This should be set to TRUE for all-day events + assertEquals("TRUE", vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT")); + } + public void testCreateMessageForEntity_Invite() throws IOException { // Set up the "event" String title = "Discuss Unit Tests"; @@ -320,6 +384,14 @@ public class CalendarUtilitiesTests extends AndroidTestCase { assertEquals(uid, vevent.get("UID")); assertEquals("MAILTO:" + ATTENDEE, vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE")); + + // These next two fields should exist (without the VALUE=DATE suffix) + assertNotNull(vevent.get("DTSTART")); + assertNotNull(vevent.get("DTEND")); + assertNull(vevent.get("DTSTART;VALUE=DATE")); + assertNull(vevent.get("DTEND;VALUE=DATE")); + // This shouldn't exist for this event + assertNull(vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT")); } public void testCreateMessageForEntity_Exception_Cancel() throws IOException { @@ -396,8 +468,8 @@ public class CalendarUtilitiesTests extends AndroidTestCase { long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME); assertNotSame(0, originalTime); // For an exception, RECURRENCE-ID is critical - assertEquals(CalendarUtilities.millisToEasDateTime(originalTime, timeZone), - vevent.get("RECURRENCE-ID" + ";TZID=" + timeZone.getID())); + assertEquals(CalendarUtilities.millisToEasDateTime(originalTime, timeZone, + true /*withTime*/), vevent.get("RECURRENCE-ID" + ";TZID=" + timeZone.getID())); } public void testUtcOffsetString() {