Home | History | Annotate | Download | only in utility
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.exchange.utility;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Entity;
     21 import android.content.res.Resources;
     22 import android.provider.CalendarContract.Attendees;
     23 import android.provider.CalendarContract.Events;
     24 import android.test.suitebuilder.annotation.MediumTest;
     25 import android.util.Log;
     26 
     27 import com.android.emailcommon.mail.Address;
     28 import com.android.emailcommon.provider.Account;
     29 import com.android.emailcommon.provider.EmailContent.Attachment;
     30 import com.android.emailcommon.provider.EmailContent.Message;
     31 import com.android.emailcommon.utility.Utility;
     32 import com.android.exchange.R;
     33 import com.android.exchange.adapter.CalendarSyncAdapter;
     34 import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
     35 import com.android.exchange.adapter.Parser;
     36 import com.android.exchange.adapter.Serializer;
     37 import com.android.exchange.adapter.SyncAdapterTestCase;
     38 import com.android.exchange.adapter.Tags;
     39 
     40 import java.io.BufferedReader;
     41 import java.io.ByteArrayInputStream;
     42 import java.io.IOException;
     43 import java.io.StringReader;
     44 import java.text.DateFormat;
     45 import java.util.ArrayList;
     46 import java.util.Calendar;
     47 import java.util.Date;
     48 import java.util.GregorianCalendar;
     49 import java.util.HashMap;
     50 import 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
     61 public 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 (at) server.com";
    114     private static final String ATTENDEE = "attendee (at) 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