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