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