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