• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.exchange.utility;
18 
19 import com.android.email.R;
20 import com.android.email.Utility;
21 import com.android.email.mail.Address;
22 import com.android.email.provider.EmailContent.Account;
23 import com.android.email.provider.EmailContent.Attachment;
24 import com.android.email.provider.EmailContent.Message;
25 
26 import android.content.ContentValues;
27 import android.content.Entity;
28 import android.content.res.Resources;
29 import android.provider.Calendar.Attendees;
30 import android.provider.Calendar.Events;
31 import android.test.AndroidTestCase;
32 import android.util.Log;
33 
34 import java.io.BufferedReader;
35 import java.io.IOException;
36 import java.io.StringReader;
37 import java.text.DateFormat;
38 import java.util.ArrayList;
39 import java.util.Calendar;
40 import java.util.Date;
41 import java.util.GregorianCalendar;
42 import java.util.HashMap;
43 import java.util.TimeZone;
44 
45 /**
46  * Tests of EAS Calendar Utilities
47  * You can run this entire test case with:
48  *   runtest -c com.android.exchange.utility.CalendarUtilitiesTests email
49  *
50  * Please see RFC2445 for RRULE definition
51  * http://www.ietf.org/rfc/rfc2445.txt
52  */
53 
54 public class CalendarUtilitiesTests extends AndroidTestCase {
55 
56     // Some prebuilt time zones, Base64 encoded (as they arrive from EAS)
57     // More time zones to be added over time
58 
59     // Not all time zones are appropriate for testing.  For example, ISRAEL_STANDARD_TIME cannot be
60     // used because DST is determined from year to year in a non-standard way (related to the lunar
61     // calendar); therefore, the test would only work during the year in which it was created
62 
63     // This time zone has no DST
64     private static final String ASIA_CALCUTTA_TIME =
65         "tv7//0kAbgBkAGkAYQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
66         "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAbgBkAGkAYQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUA" +
67         "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
68 
69     // This time zone is equivalent to PST and uses DST
70     private static final String AMERICA_DAWSON_TIME =
71         "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
72         "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
73         "bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
74 
75     // Test a southern hemisphere time zone w/ DST
76     private static final String AUSTRALIA_ACT_TIME =
77         "qP3//0EAVQBTACAARQBhAHMAdABlAHIAbgAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAA" +
78         "AAAAAAAAAAQAAAABAAMAAAAAAAAAAAAAAEEAVQBTACAARQBhAHMAdABlAHIAbgAgAEQAYQB5AGwAaQBnAGgA" +
79         "dAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAoAAAABAAIAAAAAAAAAxP///w==";
80 
81     // Test a european time zone w/ DST
82     private static final String EUROPE_MOSCOW_TIME =
83         "TP///1IAdQBzAHMAaQBhAG4AIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
84         "AAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAFIAdQBzAHMAaQBhAG4AIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
85         "bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAFAAIAAAAAAAAAxP///w==";
86 
87     // Test a timezone with GMT bias but bogus DST parameters (there is no equivalent time zone
88     // in the database)
89     private static final String GMT_UNKNOWN_DAYLIGHT_TIME =
90         "AAAAACgARwBNAFQAKwAwADAAOgAwADAAKQAgAFQAaQBtAGUAIABaAG8AbgBlAAAAAAAAAAAAAAAAAAAAAAAA" +
91         "AAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAACgARwBNAFQAKwAwADAAOgAwADAAKQAgAFQAaQBtAGUAIABaAG8A" +
92         "bgBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAEAAAAAAAAAxP///w==";
93 
94     // This time zone has no DST, but earlier, buggy code retrieved a TZ WITH DST
95     private static final String ARIZONA_TIME =
96         "pAEAAFUAUwAgAE0AbwB1AG4AdABhAGkAbgAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAA" +
97         "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFUAUwAgAE0AbwB1AG4AdABhAGkAbgAgAEQAYQB5AGwAaQBnAGgA" +
98         "dAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
99 
100     private static final String ORGANIZER = "organizer@server.com";
101     private static final String ATTENDEE = "attendee@server.com";
102 
testGetSet()103     public void testGetSet() {
104         byte[] bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7};
105 
106         // First, check that getWord/Long are properly little endian
107         assertEquals(0x0100, CalendarUtilities.getWord(bytes, 0));
108         assertEquals(0x03020100, CalendarUtilities.getLong(bytes, 0));
109         assertEquals(0x07060504, CalendarUtilities.getLong(bytes, 4));
110 
111         // Set some words and longs
112         CalendarUtilities.setWord(bytes, 0, 0xDEAD);
113         CalendarUtilities.setLong(bytes, 2, 0xBEEFBEEF);
114         CalendarUtilities.setWord(bytes, 6, 0xCEDE);
115 
116         // Retrieve them
117         assertEquals(0xDEAD, CalendarUtilities.getWord(bytes, 0));
118         assertEquals(0xBEEFBEEF, CalendarUtilities.getLong(bytes, 2));
119         assertEquals(0xCEDE, CalendarUtilities.getWord(bytes, 6));
120     }
121 
testParseTimeZoneEndToEnd()122     public void testParseTimeZoneEndToEnd() {
123         TimeZone tz = CalendarUtilities.tziStringToTimeZone(AMERICA_DAWSON_TIME);
124         assertEquals("America/Dawson", tz.getID());
125         tz = CalendarUtilities.tziStringToTimeZone(ASIA_CALCUTTA_TIME);
126         assertEquals("Asia/Calcutta", tz.getID());
127         tz = CalendarUtilities.tziStringToTimeZone(AUSTRALIA_ACT_TIME);
128         assertEquals("Australia/ACT", tz.getID());
129         tz = CalendarUtilities.tziStringToTimeZone(EUROPE_MOSCOW_TIME);
130         assertEquals("Europe/Moscow", tz.getID());
131         tz = CalendarUtilities.tziStringToTimeZone(GMT_UNKNOWN_DAYLIGHT_TIME);
132         int bias = tz.getOffset(System.currentTimeMillis());
133         assertEquals(0, bias);
134         // Make sure non-DST TZ's work properly
135         tz = CalendarUtilities.tziStringToTimeZone(ARIZONA_TIME);
136         assertEquals("America/Phoenix", tz.getID());
137     }
138 
testGenerateEasDayOfWeek()139     public void testGenerateEasDayOfWeek() {
140         String byDay = "TU,WE,SA";
141         // TU = 4, WE = 8; SA = 64;
142         assertEquals("76", CalendarUtilities.generateEasDayOfWeek(byDay));
143         // MO = 2, TU = 4; WE = 8; TH = 16; FR = 32
144         byDay = "MO,TU,WE,TH,FR";
145         assertEquals("62", CalendarUtilities.generateEasDayOfWeek(byDay));
146         // SU = 1
147         byDay = "SU";
148         assertEquals("1", CalendarUtilities.generateEasDayOfWeek(byDay));
149     }
150 
testTokenFromRrule()151     public void testTokenFromRrule() {
152         String rrule = "FREQ=DAILY;INTERVAL=1;BYDAY=WE,TH,SA;BYMONTHDAY=17";
153         assertEquals("DAILY", CalendarUtilities.tokenFromRrule(rrule, "FREQ="));
154         assertEquals("1", CalendarUtilities.tokenFromRrule(rrule, "INTERVAL="));
155         assertEquals("17", CalendarUtilities.tokenFromRrule(rrule, "BYMONTHDAY="));
156         assertEquals("WE,TH,SA", CalendarUtilities.tokenFromRrule(rrule, "BYDAY="));
157         assertNull(CalendarUtilities.tokenFromRrule(rrule, "UNTIL="));
158     }
159 
testRecurrenceUntilToEasUntil()160     public void testRecurrenceUntilToEasUntil() {
161         // Test full format
162         assertEquals("YYYYMMDDT000000Z",
163                 CalendarUtilities.recurrenceUntilToEasUntil("YYYYMMDDTHHMMSSZ"));
164         // Test date only format
165         assertEquals("YYYYMMDDT000000Z",
166                 CalendarUtilities.recurrenceUntilToEasUntil("YYYYMMDD"));
167     }
168 
testParseEmailDateTimeToMillis(String date)169     public void testParseEmailDateTimeToMillis(String date) {
170         // Format for email date strings is 2010-02-23T16:00:00.000Z
171         String dateString = "2010-02-23T15:16:17.000Z";
172         long dateTime = Utility.parseEmailDateTimeToMillis(dateString);
173         GregorianCalendar cal = new GregorianCalendar();
174         cal.setTimeInMillis(dateTime);
175         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
176         assertEquals(cal.get(Calendar.YEAR), 2010);
177         assertEquals(cal.get(Calendar.MONTH), 1);  // 0 based
178         assertEquals(cal.get(Calendar.DAY_OF_MONTH), 23);
179         assertEquals(cal.get(Calendar.HOUR_OF_DAY), 16);
180         assertEquals(cal.get(Calendar.MINUTE), 16);
181         assertEquals(cal.get(Calendar.SECOND), 17);
182     }
183 
testParseDateTimeToMillis(String date)184     public void testParseDateTimeToMillis(String date) {
185         // Format for calendar date strings is 20100223T160000000Z
186         String dateString = "20100223T151617000Z";
187         long dateTime = Utility.parseDateTimeToMillis(dateString);
188         GregorianCalendar cal = new GregorianCalendar();
189         cal.setTimeInMillis(dateTime);
190         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
191         assertEquals(cal.get(Calendar.YEAR), 2010);
192         assertEquals(cal.get(Calendar.MONTH), 1);  // 0 based
193         assertEquals(cal.get(Calendar.DAY_OF_MONTH), 23);
194         assertEquals(cal.get(Calendar.HOUR_OF_DAY), 16);
195         assertEquals(cal.get(Calendar.MINUTE), 16);
196         assertEquals(cal.get(Calendar.SECOND), 17);
197     }
198 
setupTestEventEntity(String organizer, String attendee, String title)199     private Entity setupTestEventEntity(String organizer, String attendee, String title) {
200         // Create an Entity for an Event
201         ContentValues entityValues = new ContentValues();
202         Entity entity = new Entity(entityValues);
203 
204         // Set up values for the Event
205         String location = "Meeting Location";
206 
207         // Fill in times, location, title, and organizer
208         entityValues.put("DTSTAMP",
209                 CalendarUtilities.convertEmailDateTimeToCalendarDateTime("2010-04-05T14:30:51Z"));
210         entityValues.put(Events.DTSTART,
211                 Utility.parseEmailDateTimeToMillis("2010-04-12T18:30:00Z"));
212         entityValues.put(Events.DTEND,
213                 Utility.parseEmailDateTimeToMillis("2010-04-12T19:30:00Z"));
214         entityValues.put(Events.EVENT_LOCATION, location);
215         entityValues.put(Events.TITLE, title);
216         entityValues.put(Events.ORGANIZER, organizer);
217         entityValues.put(Events._SYNC_DATA, "31415926535");
218 
219         // Add the attendee
220         ContentValues attendeeValues = new ContentValues();
221         attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
222         attendeeValues.put(Attendees.ATTENDEE_EMAIL, attendee);
223         entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
224 
225         // Add the organizer
226         ContentValues organizerValues = new ContentValues();
227         organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
228         organizerValues.put(Attendees.ATTENDEE_EMAIL, organizer);
229         entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
230         return entity;
231     }
232 
setupTestExceptionEntity(String organizer, String attendee, String title)233     private Entity setupTestExceptionEntity(String organizer, String attendee, String title) {
234         Entity entity = setupTestEventEntity(organizer, attendee, title);
235         ContentValues entityValues = entity.getEntityValues();
236         entityValues.put(Events.ORIGINAL_EVENT, 69);
237         // The exception will be on April 26th
238         entityValues.put(Events.ORIGINAL_INSTANCE_TIME,
239                 Utility.parseEmailDateTimeToMillis("2010-04-26T18:30:00Z"));
240         return entity;
241     }
242 
testCreateMessageForEntity_Reply()243     public void testCreateMessageForEntity_Reply() {
244         // Set up the "event"
245         String title = "Discuss Unit Tests";
246         Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
247 
248         // Create a dummy account for the attendee
249         Account account = new Account();
250         account.mEmailAddress = ATTENDEE;
251 
252         // The uid is required, but can be anything
253         String uid = "31415926535";
254 
255         // Create the outgoing message
256         Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
257                 Message.FLAG_OUTGOING_MEETING_ACCEPT, uid, account);
258 
259         // First, we should have a message
260         assertNotNull(msg);
261 
262         // Now check some of the fields of the message
263         assertEquals(Address.pack(new Address[] {new Address(ORGANIZER)}), msg.mTo);
264         Resources resources = getContext().getResources();
265         String accept = resources.getString(R.string.meeting_accepted, title);
266         assertEquals(accept, msg.mSubject);
267         assertNotNull(msg.mText);
268         assertTrue(msg.mText.contains(resources.getString(R.string.meeting_where, "")));
269 
270         // And make sure we have an attachment
271         assertNotNull(msg.mAttachments);
272         assertEquals(1, msg.mAttachments.size());
273         Attachment att = msg.mAttachments.get(0);
274         // And that the attachment has the correct elements
275         assertEquals("invite.ics", att.mFileName);
276         assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
277                 att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
278         assertEquals("text/calendar; method=REPLY", att.mMimeType);
279         assertNotNull(att.mContentBytes);
280         assertEquals(att.mSize, att.mContentBytes.length);
281 
282         //TODO Check the contents of the attachment using an iCalendar parser
283     }
284 
testCreateMessageForEntity_Invite_AllDay()285     public void testCreateMessageForEntity_Invite_AllDay() throws IOException {
286         // Set up the "event"
287         String title = "Discuss Unit Tests";
288         Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
289         entity.getEntityValues().put(Events.ALL_DAY, 1);
290 
291         // Create a dummy account for the attendee
292         Account account = new Account();
293         account.mEmailAddress = ORGANIZER;
294 
295         // The uid is required, but can be anything
296         String uid = "31415926535";
297 
298         // Create the outgoing message
299         Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
300                 Message.FLAG_OUTGOING_MEETING_INVITE, uid, account);
301 
302         // First, we should have a message
303         assertNotNull(msg);
304 
305         // Now check some of the fields of the message
306         assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
307         assertEquals(title, msg.mSubject);
308 
309         // And make sure we have an attachment
310         assertNotNull(msg.mAttachments);
311         assertEquals(1, msg.mAttachments.size());
312         Attachment att = msg.mAttachments.get(0);
313         // And that the attachment has the correct elements
314         assertEquals("invite.ics", att.mFileName);
315         assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
316                 att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
317         assertEquals("text/calendar; method=REQUEST", att.mMimeType);
318         assertNotNull(att.mContentBytes);
319         assertEquals(att.mSize, att.mContentBytes.length);
320 
321         // We'll check the contents of the ics file here
322         BlockHash vcalendar = parseIcsContent(att.mContentBytes);
323         assertNotNull(vcalendar);
324 
325         // We should have a VCALENDAR with a REQUEST method
326         assertEquals("VCALENDAR", vcalendar.name);
327         assertEquals("REQUEST", vcalendar.get("METHOD"));
328 
329         // We should have one block under VCALENDAR
330         assertEquals(1, vcalendar.blocks.size());
331         BlockHash vevent = vcalendar.blocks.get(0);
332         // It's a VEVENT with the following fields
333         assertEquals("VEVENT", vevent.name);
334         assertEquals("Meeting Location", vevent.get("LOCATION"));
335         assertEquals("0", vevent.get("SEQUENCE"));
336         assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
337         assertEquals(uid, vevent.get("UID"));
338         assertEquals("MAILTO:" + ATTENDEE,
339                 vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"));
340 
341         // These next two fields should have a date only
342         assertEquals("20100412", vevent.get("DTSTART;VALUE=DATE"));
343         assertEquals("20100412", vevent.get("DTEND;VALUE=DATE"));
344         // This should be set to TRUE for all-day events
345         assertEquals("TRUE", vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT"));
346     }
347 
testCreateMessageForEntity_Invite()348     public void testCreateMessageForEntity_Invite() throws IOException {
349         // Set up the "event"
350         String title = "Discuss Unit Tests";
351         Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
352 
353         // Create a dummy account for the attendee
354         Account account = new Account();
355         account.mEmailAddress = ORGANIZER;
356 
357         // The uid is required, but can be anything
358         String uid = "31415926535";
359 
360         // Create the outgoing message
361         Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
362                 Message.FLAG_OUTGOING_MEETING_INVITE, uid, account);
363 
364         // First, we should have a message
365         assertNotNull(msg);
366 
367         // Now check some of the fields of the message
368         assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
369         assertEquals(title, msg.mSubject);
370 
371         // And make sure we have an attachment
372         assertNotNull(msg.mAttachments);
373         assertEquals(1, msg.mAttachments.size());
374         Attachment att = msg.mAttachments.get(0);
375         // And that the attachment has the correct elements
376         assertEquals("invite.ics", att.mFileName);
377         assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
378                 att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
379         assertEquals("text/calendar; method=REQUEST", att.mMimeType);
380         assertNotNull(att.mContentBytes);
381         assertEquals(att.mSize, att.mContentBytes.length);
382 
383         // We'll check the contents of the ics file here
384         BlockHash vcalendar = parseIcsContent(att.mContentBytes);
385         assertNotNull(vcalendar);
386 
387         // We should have a VCALENDAR with a REQUEST method
388         assertEquals("VCALENDAR", vcalendar.name);
389         assertEquals("REQUEST", vcalendar.get("METHOD"));
390 
391         // We should have one block under VCALENDAR
392         assertEquals(1, vcalendar.blocks.size());
393         BlockHash vevent = vcalendar.blocks.get(0);
394         // It's a VEVENT with the following fields
395         assertEquals("VEVENT", vevent.name);
396         assertEquals("Meeting Location", vevent.get("LOCATION"));
397         assertEquals("0", vevent.get("SEQUENCE"));
398         assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
399         assertEquals(uid, vevent.get("UID"));
400         assertEquals("MAILTO:" + ATTENDEE,
401                 vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"));
402 
403         // These next two fields should exist (without the VALUE=DATE suffix)
404         assertNotNull(vevent.get("DTSTART"));
405         assertNotNull(vevent.get("DTEND"));
406         assertNull(vevent.get("DTSTART;VALUE=DATE"));
407         assertNull(vevent.get("DTEND;VALUE=DATE"));
408         // This shouldn't exist for this event
409         assertNull(vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT"));
410     }
411 
testCreateMessageForEntity_Recurring()412     public void testCreateMessageForEntity_Recurring() throws IOException {
413         // Set up the "event"
414         String title = "Discuss Unit Tests";
415         Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
416         // Set up a RRULE for this event
417         entity.getEntityValues().put(Events.RRULE, "FREQ=DAILY");
418 
419         // Create a dummy account for the attendee
420         Account account = new Account();
421         account.mEmailAddress = ORGANIZER;
422 
423         // The uid is required, but can be anything
424         String uid = "31415926535";
425 
426         // Create the outgoing message
427         Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
428                 Message.FLAG_OUTGOING_MEETING_INVITE, uid, account);
429 
430         // First, we should have a message
431         assertNotNull(msg);
432 
433         // Now check some of the fields of the message
434         assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
435         assertEquals(title, msg.mSubject);
436 
437         // And make sure we have an attachment
438         assertNotNull(msg.mAttachments);
439         assertEquals(1, msg.mAttachments.size());
440         Attachment att = msg.mAttachments.get(0);
441         // And that the attachment has the correct elements
442         assertEquals("invite.ics", att.mFileName);
443         assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
444                 att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
445         assertEquals("text/calendar; method=REQUEST", att.mMimeType);
446         assertNotNull(att.mContentBytes);
447         assertEquals(att.mSize, att.mContentBytes.length);
448 
449         // We'll check the contents of the ics file here
450         BlockHash vcalendar = parseIcsContent(att.mContentBytes);
451         assertNotNull(vcalendar);
452 
453         // We should have a VCALENDAR with a REQUEST method
454         assertEquals("VCALENDAR", vcalendar.name);
455         assertEquals("REQUEST", vcalendar.get("METHOD"));
456 
457         // We should have two blocks under VCALENDAR (VTIMEZONE and VEVENT)
458         assertEquals(2, vcalendar.blocks.size());
459 
460         // This is the time zone that should be used
461         TimeZone timeZone = TimeZone.getDefault();
462 
463         BlockHash vtimezone = vcalendar.blocks.get(0);
464         // It should be a VTIMEZONE for timeZone
465         assertEquals("VTIMEZONE", vtimezone.name);
466         assertEquals(timeZone.getID(), vtimezone.get("TZID"));
467 
468         BlockHash vevent = vcalendar.blocks.get(1);
469         // It's a VEVENT with the following fields
470         assertEquals("VEVENT", vevent.name);
471         assertEquals("Meeting Location", vevent.get("LOCATION"));
472         assertEquals("0", vevent.get("SEQUENCE"));
473         assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
474         assertEquals(uid, vevent.get("UID"));
475         assertEquals("MAILTO:" + ATTENDEE,
476                 vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"));
477 
478         // We should have DTSTART/DTEND with time zone
479         assertNotNull(vevent.get("DTSTART;TZID=" + timeZone.getID()));
480         assertNotNull(vevent.get("DTEND;TZID=" + timeZone.getID()));
481         assertNull(vevent.get("DTSTART"));
482         assertNull(vevent.get("DTEND"));
483         assertNull(vevent.get("DTSTART;VALUE=DATE"));
484         assertNull(vevent.get("DTEND;VALUE=DATE"));
485         // This shouldn't exist for this event
486         assertNull(vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT"));
487     }
488 
testCreateMessageForEntity_Exception_Cancel()489     public void testCreateMessageForEntity_Exception_Cancel() throws IOException {
490         // Set up the "exception"...
491         String title = "Discuss Unit Tests";
492         Entity entity = setupTestExceptionEntity(ORGANIZER, ATTENDEE, title);
493 
494         ContentValues entityValues = entity.getEntityValues();
495         // Mark the Exception as dirty
496         entityValues.put(Events._SYNC_DIRTY, 1);
497         // And mark it canceled
498         entityValues.put(Events.STATUS, Events.STATUS_CANCELED);
499 
500         // Create a dummy account for the attendee
501         Account account = new Account();
502         account.mEmailAddress = ORGANIZER;
503 
504         // The uid is required, but can be anything
505         String uid = "31415926535";
506 
507         // Create the outgoing message
508         Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
509                 Message.FLAG_OUTGOING_MEETING_CANCEL, uid, account);
510 
511         // First, we should have a message
512         assertNotNull(msg);
513 
514         // Now check some of the fields of the message
515         assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
516         String cancel = getContext().getResources().getString(R.string.meeting_canceled, title);
517         assertEquals(cancel, msg.mSubject);
518 
519         // And make sure we have an attachment
520         assertNotNull(msg.mAttachments);
521         assertEquals(1, msg.mAttachments.size());
522         Attachment att = msg.mAttachments.get(0);
523         // And that the attachment has the correct elements
524         assertEquals("invite.ics", att.mFileName);
525         assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
526                 att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
527         assertEquals("text/calendar; method=CANCEL", att.mMimeType);
528         assertNotNull(att.mContentBytes);
529 
530         // We'll check the contents of the ics file here
531         BlockHash vcalendar = parseIcsContent(att.mContentBytes);
532         assertNotNull(vcalendar);
533 
534         // We should have a VCALENDAR with a CANCEL method
535         assertEquals("VCALENDAR", vcalendar.name);
536         assertEquals("CANCEL", vcalendar.get("METHOD"));
537 
538         // This is the time zone that should be used
539         TimeZone timeZone = TimeZone.getDefault();
540 
541         // We should have two blocks under VCALENDAR (VTIMEZONE and VEVENT)
542         assertEquals(2, vcalendar.blocks.size());
543 
544         BlockHash vtimezone = vcalendar.blocks.get(0);
545         // It should be a VTIMEZONE for timeZone
546         assertEquals("VTIMEZONE", vtimezone.name);
547         assertEquals(timeZone.getID(), vtimezone.get("TZID"));
548 
549         BlockHash vevent = vcalendar.blocks.get(1);
550         // It's a VEVENT with the following fields
551         assertEquals("VEVENT", vevent.name);
552         assertEquals("Meeting Location", vevent.get("LOCATION"));
553         assertEquals("0", vevent.get("SEQUENCE"));
554         assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
555         assertEquals(uid, vevent.get("UID"));
556         assertEquals("MAILTO:" + ATTENDEE,
557                 vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT"));
558         long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
559         assertNotSame(0, originalTime);
560         // For an exception, RECURRENCE-ID is critical
561         assertEquals(CalendarUtilities.millisToEasDateTime(originalTime, timeZone,
562                 true /*withTime*/), vevent.get("RECURRENCE-ID" + ";TZID=" + timeZone.getID()));
563     }
564 
testUtcOffsetString()565     public void testUtcOffsetString() {
566         assertEquals(CalendarUtilities.utcOffsetString(540), "+0900");
567         assertEquals(CalendarUtilities.utcOffsetString(-480), "-0800");
568         assertEquals(CalendarUtilities.utcOffsetString(0), "+0000");
569     }
570 
testFindTransitionDate()571     public void testFindTransitionDate() {
572         // We'll find some transitions and make sure that we're properly in or out of daylight time
573         // on either side of the transition.
574         // Use CST for testing (any other will do as well, as long as it has DST)
575         TimeZone tz = TimeZone.getTimeZone("US/Central");
576         // Get a calendar at January 1st of the current year
577         GregorianCalendar calendar = new GregorianCalendar(tz);
578         calendar.set(CalendarUtilities.sCurrentYear, Calendar.JANUARY, 1);
579         // Get start and end times at start and end of year
580         long startTime = calendar.getTimeInMillis();
581         long endTime = startTime + (365*CalendarUtilities.DAYS);
582         // Find the first transition
583         GregorianCalendar transitionCalendar =
584             CalendarUtilities.findTransitionDate(tz, startTime, endTime, false);
585         long transitionTime = transitionCalendar.getTimeInMillis();
586         // Before should be in standard time; after in daylight time
587         Date beforeDate = new Date(transitionTime - CalendarUtilities.HOURS);
588         Date afterDate = new Date(transitionTime + CalendarUtilities.HOURS);
589         assertFalse(tz.inDaylightTime(beforeDate));
590         assertTrue(tz.inDaylightTime(afterDate));
591 
592         // Find the next one...
593         transitionCalendar = CalendarUtilities.findTransitionDate(tz, transitionTime +
594                 CalendarUtilities.DAYS, endTime, true);
595         transitionTime = transitionCalendar.getTimeInMillis();
596         // This time, Before should be in daylight time; after in standard time
597         beforeDate = new Date(transitionTime - CalendarUtilities.HOURS);
598         afterDate = new Date(transitionTime + CalendarUtilities.HOURS);
599         assertTrue(tz.inDaylightTime(beforeDate));
600         assertFalse(tz.inDaylightTime(afterDate));
601 
602         // Captain Renault: What in heaven's name brought you to Casablanca?
603         // Rick: My health. I came to Casablanca for the waters.
604         // Also, they have no daylight savings time
605         tz = TimeZone.getTimeZone("Africa/Casablanca");
606         // Get a calendar at January 1st of the current year
607         calendar = new GregorianCalendar(tz);
608         calendar.set(CalendarUtilities.sCurrentYear, Calendar.JANUARY, 1);
609         // Get start and end times at start and end of year
610         startTime = calendar.getTimeInMillis();
611         endTime = startTime + (365*CalendarUtilities.DAYS);
612         // Find the first transition
613         transitionCalendar = CalendarUtilities.findTransitionDate(tz, startTime, endTime, false);
614         // There had better not be one
615         assertNull(transitionCalendar);
616     }
617 
testRruleFromRecurrence()618     public void testRruleFromRecurrence() {
619         // Every Monday for 2 weeks
620         String rrule = CalendarUtilities.rruleFromRecurrence(
621                 1 /*Weekly*/, 2 /*Occurrences*/, 1 /*Interval*/, 2 /*Monday*/, 0, 0, 0, null);
622         assertEquals("FREQ=WEEKLY;INTERVAL=1;COUNT=2;BYDAY=MO", rrule);
623         // Every Tuesday and Friday
624         rrule = CalendarUtilities.rruleFromRecurrence(
625                 1 /*Weekly*/, 0 /*Occurrences*/, 0 /*Interval*/, 36 /*Tue&Fri*/, 0, 0, 0, null);
626         assertEquals("FREQ=WEEKLY;BYDAY=TU,FR", rrule);
627         // The last Saturday of the month
628         rrule = CalendarUtilities.rruleFromRecurrence(
629                 3 /*Monthly/DayofWeek*/, 0, 0, 64 /*Sat*/, 0, 5 /*Last*/, 0, null);
630         assertEquals("FREQ=MONTHLY;BYDAY=-1SA", rrule);
631         // The third Wednesday and Thursday of the month
632         rrule = CalendarUtilities.rruleFromRecurrence(
633                 3 /*Monthly/DayofWeek*/, 0, 0, 24 /*Wed&Thu*/, 0, 3 /*3rd*/, 0, null);
634         assertEquals("FREQ=MONTHLY;BYDAY=3WE,3TH", rrule);
635         // The 14th of the every month
636         rrule = CalendarUtilities.rruleFromRecurrence(
637                 2 /*Monthly/Date*/, 0, 0, 0, 14 /*14th*/, 0, 0, null);
638         assertEquals("FREQ=MONTHLY;BYMONTHDAY=14", rrule);
639         // Every 31st of October
640         rrule = CalendarUtilities.rruleFromRecurrence(
641                 5 /*Yearly/Date*/, 0, 0, 0, 31 /*31st*/, 0, 10 /*October*/, null);
642         assertEquals("FREQ=YEARLY;BYMONTHDAY=31;BYMONTH=10", rrule);
643         // The first Tuesday of June
644         rrule = CalendarUtilities.rruleFromRecurrence(
645                 6 /*Yearly/Month/DayOfWeek*/, 0, 0, 4 /*Tue*/, 0, 1 /*1st*/, 6 /*June*/, null);
646         assertEquals("FREQ=YEARLY;BYDAY=1TU;BYMONTH=6", rrule);
647     }
648 
649     /**
650      * For debugging purposes, to help keep track of parsing errors.
651      */
652     private class UnterminatedBlockException extends IOException {
653         private static final long serialVersionUID = 1L;
UnterminatedBlockException(String name)654         UnterminatedBlockException(String name) {
655             super(name);
656         }
657     }
658 
659     /**
660      * A lightweight representation of block object containing a hash of individual values and an
661      * array of inner blocks.  The object is build by pulling elements from a BufferedReader.
662      * NOTE: Multiple values of a given field are not supported.  We'd see this with ATTENDEEs, for
663      * example, and possibly RDATEs in VTIMEZONEs without an RRULE; these cases will be handled
664      * at a later time.
665      */
666     private class BlockHash {
667         String name;
668         HashMap<String, String> hash = new HashMap<String, String>();
669         ArrayList<BlockHash> blocks = new ArrayList<BlockHash>();
670 
BlockHash(String _name, BufferedReader reader)671         BlockHash (String _name, BufferedReader reader) throws IOException {
672             name = _name;
673             String lastField = null;
674             String lastValue = null;
675             while (true) {
676                 // Get a line; we're done if it's null
677                 String line = reader.readLine();
678                 if (line == null) {
679                     throw new UnterminatedBlockException(name);
680                 }
681                 int length = line.length();
682                 if (length == 0) {
683                     // We shouldn't ever see an empty line
684                     throw new IllegalArgumentException();
685                 }
686                 // A line starting with tab is a continuation
687                 if (line.charAt(0) == '\t') {
688                     // Remember the line and length
689                     lastValue = line.substring(1);
690                     // Save the concatenation of old and new values
691                     hash.put(lastField, hash.get(lastField) + lastValue);
692                     continue;
693                 }
694                 // Find the field delimiter
695                 int pos = line.indexOf(':');
696                 // If not found, or at EOL, this is a bad ics
697                 if (pos < 0 || pos >= length) {
698                     throw new IllegalArgumentException();
699                 }
700                 // Remember the field, value, and length
701                 lastField = line.substring(0, pos);
702                 lastValue = line.substring(pos + 1);
703                 if (lastField.equals("BEGIN")) {
704                     blocks.add(new BlockHash(lastValue, reader));
705                     continue;
706                 } else if (lastField.equals("END")) {
707                     if (!lastValue.equals(name)) {
708                         throw new UnterminatedBlockException(name);
709                     }
710                     break;
711                 }
712 
713                 // Save it away and continue
714                 hash.put(lastField, lastValue);
715             }
716         }
717 
get(String field)718         String get(String field) {
719             return hash.get(field);
720         }
721     }
722 
parseIcsContent(byte[] bytes)723     private BlockHash parseIcsContent(byte[] bytes) throws IOException {
724         BufferedReader reader = new BufferedReader(new StringReader(Utility.fromUtf8(bytes)));
725         String line = reader.readLine();
726         if (!line.equals("BEGIN:VCALENDAR")) {
727             throw new IllegalArgumentException();
728         }
729         return new BlockHash("VCALENDAR", reader);
730     }
731 
testBuildMessageTextFromEntityValues()732     public void testBuildMessageTextFromEntityValues() {
733         // Set up a test event
734         String title = "Event Title";
735         Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
736         ContentValues entityValues = entity.getEntityValues();
737 
738         // Save this away; we'll use it a few times below
739         Resources resources = mContext.getResources();
740         Date date = new Date(entityValues.getAsLong(Events.DTSTART));
741         String dateTimeString = DateFormat.getDateTimeInstance().format(date);
742 
743         // Get the text for this message
744         StringBuilder sb = new StringBuilder();
745         CalendarUtilities.buildMessageTextFromEntityValues(mContext, entityValues, sb);
746         String text = sb.toString();
747         // We'll just check the when and where
748         assertTrue(text.contains(resources.getString(R.string.meeting_when, dateTimeString)));
749         String location = entityValues.getAsString(Events.EVENT_LOCATION);
750         assertTrue(text.contains(resources.getString(R.string.meeting_where, location)));
751 
752         // Make this event recurring
753         entity.getEntityValues().put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO");
754         sb = new StringBuilder();
755         CalendarUtilities.buildMessageTextFromEntityValues(mContext, entityValues, sb);
756         text = sb.toString();
757         assertTrue(text.contains(resources.getString(R.string.meeting_recurring, dateTimeString)));
758     }
759 
760     /**
761      * Sanity test for time zone generation.  Most important, make sure that we can run through
762      * all of the time zones without generating an exception.  Second, make sure that we're finding
763      * rules for at least 90% of time zones that use daylight time (empirically, it's more like
764      * 95%).  Log those without rules.
765      * @throws IOException
766      */
testTimeZoneToVTimezone()767     public void testTimeZoneToVTimezone() throws IOException {
768         SimpleIcsWriter writer = new SimpleIcsWriter();
769         int rule = 0;
770         int nodst = 0;
771         int norule = 0;
772         ArrayList<String> norulelist = new ArrayList<String>();
773         for (String tzs: TimeZone.getAvailableIDs()) {
774             TimeZone tz = TimeZone.getTimeZone(tzs);
775             writer = new SimpleIcsWriter();
776             CalendarUtilities.timeZoneToVTimezone(tz, writer);
777             String vc = writer.toString();
778             boolean hasRule = vc.indexOf("RRULE") > 0;
779             if (hasRule) {
780                 rule++;
781             } else if (tz.useDaylightTime()) {
782                 norule++;
783                 norulelist.add(tz.getID());
784             } else {
785                 nodst++;
786             }
787         }
788         assertTrue(norule < rule/10);
789         Log.d("TimeZoneGeneration",
790                 "Rule: " + rule + ", No DST: " + nodst + ", No rule: " + norule);
791         for (String nr: norulelist) {
792             Log.d("TimeZoneGeneration", "No rule: " + nr);
793         }
794     }
795 
796     public void testGetUidFromGlobalObjId() {
797         // This is a "foreign" uid (from some vCalendar client)
798         String globalObjId = "BAAAAIIA4AB0xbcQGoLgCAAAAAAAAAAAAAAAAAAAAAAAAAAAMQAAA" +
799                 "HZDYWwtVWlkAQAAADI3NjU1NmRkLTg1MzAtNGZiZS1iMzE0LThiM2JlYTYwMjE0OQA=";
800         String uid = CalendarUtilities.getUidFromGlobalObjId(globalObjId);
801         assertEquals(uid, "276556dd-8530-4fbe-b314-8b3bea602149");
802         // This is a native EAS uid
803         globalObjId =
804             "BAAAAIIA4AB0xbcQGoLgCAAAAADACTu7KbPKAQAAAAAAAAAAEAAAAObgsG6HVt1Fmy+7GlLbGhY=";
805         uid = CalendarUtilities.getUidFromGlobalObjId(globalObjId);
806         assertEquals(uid, "040000008200E00074C5B7101A82E00800000000C0093BBB29B3CA" +
807                 "01000000000000000010000000E6E0B06E8756DD459B2FBB1A52DB1A16");
808     }
809 
810     public void testSelfAttendeeStatusFromBusyStatus() {
811         assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED,
812                 CalendarUtilities.attendeeStatusFromBusyStatus(
813                         CalendarUtilities.BUSY_STATUS_BUSY));
814         assertEquals(Attendees.ATTENDEE_STATUS_TENTATIVE,
815                 CalendarUtilities.attendeeStatusFromBusyStatus(
816                         CalendarUtilities.BUSY_STATUS_TENTATIVE));
817         assertEquals(Attendees.ATTENDEE_STATUS_NONE,
818                 CalendarUtilities.attendeeStatusFromBusyStatus(
819                         CalendarUtilities.BUSY_STATUS_FREE));
820         assertEquals(Attendees.ATTENDEE_STATUS_NONE,
821                 CalendarUtilities.attendeeStatusFromBusyStatus(
822                         CalendarUtilities.BUSY_STATUS_OUT_OF_OFFICE));
823     }
824 
825     public void testBusyStatusFromSelfStatus() {
826         assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
827                 CalendarUtilities.busyStatusFromAttendeeStatus(
828                         Attendees.ATTENDEE_STATUS_DECLINED));
829         assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
830                 CalendarUtilities.busyStatusFromAttendeeStatus(
831                         Attendees.ATTENDEE_STATUS_NONE));
832         assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
833                 CalendarUtilities.busyStatusFromAttendeeStatus(
834                         Attendees.ATTENDEE_STATUS_INVITED));
835         assertEquals(CalendarUtilities.BUSY_STATUS_TENTATIVE,
836                 CalendarUtilities.busyStatusFromAttendeeStatus(
837                         Attendees.ATTENDEE_STATUS_TENTATIVE));
838         assertEquals(CalendarUtilities.BUSY_STATUS_BUSY,
839                 CalendarUtilities.busyStatusFromAttendeeStatus(
840                         Attendees.ATTENDEE_STATUS_ACCEPTED));
841     }
842 
843     public void testGetUtcAllDayCalendarTime() {
844         GregorianCalendar correctUtc = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
845         correctUtc.set(2011, 2, 10, 0, 0, 0);
846         long correctUtcTime = correctUtc.getTimeInMillis();
847 
848         TimeZone localTimeZone = TimeZone.getTimeZone("GMT-0700");
849         GregorianCalendar localCalendar = new GregorianCalendar(localTimeZone);
850         localCalendar.set(2011, 2, 10, 12, 23, 34);
851         long localTimeMillis = localCalendar.getTimeInMillis();
852         long convertedUtcTime =
853             CalendarUtilities.getUtcAllDayCalendarTime(localTimeMillis, localTimeZone);
854         // Milliseconds aren't zeroed out and may not be the same
855         assertEquals(convertedUtcTime/1000, correctUtcTime/1000);
856 
857         localTimeZone = TimeZone.getTimeZone("GMT+0700");
858         localCalendar = new GregorianCalendar(localTimeZone);
859         localCalendar.set(2011, 2, 10, 12, 23, 34);
860         localTimeMillis = localCalendar.getTimeInMillis();
861         convertedUtcTime =
862             CalendarUtilities.getUtcAllDayCalendarTime(localTimeMillis, localTimeZone);
863         assertEquals(convertedUtcTime/1000, correctUtcTime/1000);
864     }
865 
866     public void testGetLocalAllDayCalendarTime() {
867         TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
868         TimeZone localTimeZone = TimeZone.getTimeZone("GMT-0700");
869         GregorianCalendar correctLocal = new GregorianCalendar(localTimeZone);
870         correctLocal.set(2011, 2, 10, 0, 0, 0);
871         long correctLocalTime = correctLocal.getTimeInMillis();
872 
873         GregorianCalendar utcCalendar = new GregorianCalendar(utcTimeZone);
874         utcCalendar.set(2011, 2, 10, 12, 23, 34);
875         long utcTimeMillis = utcCalendar.getTimeInMillis();
876         long convertedLocalTime =
877             CalendarUtilities.getLocalAllDayCalendarTime(utcTimeMillis, localTimeZone);
878         // Milliseconds aren't zeroed out and may not be the same
879         assertEquals(convertedLocalTime/1000, correctLocalTime/1000);
880 
881         localTimeZone = TimeZone.getTimeZone("GMT+0700");
882         correctLocal = new GregorianCalendar(localTimeZone);
883         correctLocal.set(2011, 2, 10, 0, 0, 0);
884         correctLocalTime = correctLocal.getTimeInMillis();
885 
886         utcCalendar = new GregorianCalendar(utcTimeZone);
887         utcCalendar.set(2011, 2, 10, 12, 23, 34);
888         utcTimeMillis = utcCalendar.getTimeInMillis();
889         convertedLocalTime =
890             CalendarUtilities.getLocalAllDayCalendarTime(utcTimeMillis, localTimeZone);
891         // Milliseconds aren't zeroed out and may not be the same
892         assertEquals(convertedLocalTime/1000, correctLocalTime/1000);
893     }
894 
895     public void testGetIntegerValueAsBoolean() {
896         ContentValues cv = new ContentValues();
897         cv.put("A", 1);
898         cv.put("B", 69);
899         cv.put("C", 0);
900         assertTrue(CalendarUtilities.getIntegerValueAsBoolean(cv, "A"));
901         assertTrue(CalendarUtilities.getIntegerValueAsBoolean(cv, "B"));
902         assertFalse(CalendarUtilities.getIntegerValueAsBoolean(cv, "C"));
903         assertFalse(CalendarUtilities.getIntegerValueAsBoolean(cv, "D"));
904     }
905 }
906 
907     // TODO Planned unit tests
908     // findNextTransition
909     // recurrenceFromRrule
910     // timeZoneToTziStringImpl
911     // getDSTCalendars
912     // millisToVCalendarTime
913     // millisToEasDateTime
914     // getTrueTransitionMinute
915     // getTrueTransitionHour
916 
917