1 /* 2 * Copyright (C) 2011 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 android.provider.cts; 18 19 import android.content.BroadcastReceiver; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Entity; 25 import android.content.EntityIterator; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.cts.util.PollingCheck; 29 import android.database.Cursor; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.os.Environment; 33 import android.provider.CalendarContract; 34 import android.provider.CalendarContract.Attendees; 35 import android.provider.CalendarContract.CalendarEntity; 36 import android.provider.CalendarContract.Calendars; 37 import android.provider.CalendarContract.Colors; 38 import android.provider.CalendarContract.Events; 39 import android.provider.CalendarContract.EventsEntity; 40 import android.provider.CalendarContract.ExtendedProperties; 41 import android.provider.CalendarContract.Instances; 42 import android.provider.CalendarContract.Reminders; 43 import android.provider.CalendarContract.SyncState; 44 import android.test.InstrumentationCtsTestRunner; 45 import android.test.InstrumentationTestCase; 46 import android.test.suitebuilder.annotation.MediumTest; 47 import android.text.TextUtils; 48 import android.text.format.DateUtils; 49 import android.text.format.Time; 50 import android.util.Log; 51 52 import java.util.ArrayList; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Set; 56 57 public class CalendarTest extends InstrumentationTestCase { 58 59 private static final String TAG = "CalCTS"; 60 private static final String CTS_TEST_TYPE = "LOCAL"; 61 62 // an arbitrary int used by some tests 63 private static final int SOME_ARBITRARY_INT = 143234; 64 65 // 10 sec timeout for reminder broadcast (but shouldn't usually take this long). 66 private static final int POLLING_TIMEOUT = 10000; 67 68 // @formatter:off 69 private static final String[] TIME_ZONES = new String[] { 70 "UTC", 71 "America/Los_Angeles", 72 "Asia/Beirut", 73 "Pacific/Auckland", }; 74 // @formatter:on 75 76 private static final String SQL_WHERE_ID = Events._ID + "=?"; 77 private static final String SQL_WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?"; 78 79 private ContentResolver mContentResolver; 80 81 /** If set, log verbose instance info when running recurrence tests. */ 82 private static final boolean DEBUG_RECURRENCE = false; 83 84 private static class CalendarHelper { 85 86 // @formatter:off 87 public static final String[] CALENDARS_SYNC_PROJECTION = new String[] { 88 Calendars._ID, 89 Calendars.ACCOUNT_NAME, 90 Calendars.ACCOUNT_TYPE, 91 Calendars._SYNC_ID, 92 Calendars.CAL_SYNC7, 93 Calendars.CAL_SYNC8, 94 Calendars.DIRTY, 95 Calendars.NAME, 96 Calendars.CALENDAR_DISPLAY_NAME, 97 Calendars.CALENDAR_COLOR, 98 Calendars.CALENDAR_COLOR_KEY, 99 Calendars.CALENDAR_ACCESS_LEVEL, 100 Calendars.VISIBLE, 101 Calendars.SYNC_EVENTS, 102 Calendars.CALENDAR_LOCATION, 103 Calendars.CALENDAR_TIME_ZONE, 104 Calendars.OWNER_ACCOUNT, 105 Calendars.CAN_ORGANIZER_RESPOND, 106 Calendars.CAN_MODIFY_TIME_ZONE, 107 Calendars.MAX_REMINDERS, 108 Calendars.ALLOWED_REMINDERS, 109 Calendars.ALLOWED_AVAILABILITY, 110 Calendars.ALLOWED_ATTENDEE_TYPES, 111 Calendars.DELETED, 112 Calendars.CAL_SYNC1, 113 Calendars.CAL_SYNC2, 114 Calendars.CAL_SYNC3, 115 Calendars.CAL_SYNC4, 116 Calendars.CAL_SYNC5, 117 Calendars.CAL_SYNC6, 118 }; 119 // @formatter:on 120 121 private CalendarHelper() {} // do not instantiate this class 122 123 /** 124 * Generates the e-mail address for the Calendar owner. Use this for 125 * Calendars.OWNER_ACCOUNT, Events.OWNER_ACCOUNT, and for Attendees.ATTENDEE_EMAIL 126 * when you want a "self" attendee entry. 127 */ 128 static String generateCalendarOwnerEmail(String account) { 129 return "OWNER_" + account + "@example.com"; 130 } 131 132 /** 133 * Creates a new set of values for creating a single calendar with every 134 * field. 135 * 136 * @param account The account name to create this calendar with 137 * @param seed A number used to generate the values 138 * @return A complete set of values for the calendar 139 */ 140 public static ContentValues getNewCalendarValues( 141 String account, int seed) { 142 String seedString = Long.toString(seed); 143 ContentValues values = new ContentValues(); 144 values.put(Calendars.ACCOUNT_TYPE, CTS_TEST_TYPE); 145 146 values.put(Calendars.ACCOUNT_NAME, account); 147 values.put(Calendars._SYNC_ID, "SYNC_ID:" + seedString); 148 values.put(Calendars.CAL_SYNC7, "SYNC_V:" + seedString); 149 values.put(Calendars.CAL_SYNC8, "SYNC_TIME:" + seedString); 150 values.put(Calendars.DIRTY, 0); 151 values.put(Calendars.OWNER_ACCOUNT, generateCalendarOwnerEmail(account)); 152 153 values.put(Calendars.NAME, seedString); 154 values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString); 155 156 values.put(Calendars.CALENDAR_ACCESS_LEVEL, (seed % 8) * 100); 157 158 values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed); 159 values.put(Calendars.VISIBLE, seed % 2); 160 values.put(Calendars.SYNC_EVENTS, 1); // must be 1 for recurrence expansion 161 values.put(Calendars.CALENDAR_LOCATION, "LOCATION:" + seedString); 162 values.put(Calendars.CALENDAR_TIME_ZONE, TIME_ZONES[seed % TIME_ZONES.length]); 163 values.put(Calendars.CAN_ORGANIZER_RESPOND, seed % 2); 164 values.put(Calendars.CAN_MODIFY_TIME_ZONE, seed % 2); 165 values.put(Calendars.MAX_REMINDERS, 3); 166 values.put(Calendars.ALLOWED_REMINDERS, "0,1,2"); // does not include SMS (3) 167 values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "0,1,2,3"); 168 values.put(Calendars.ALLOWED_AVAILABILITY, "0,1,2,3"); 169 values.put(Calendars.CAL_SYNC1, "SYNC1:" + seedString); 170 values.put(Calendars.CAL_SYNC2, "SYNC2:" + seedString); 171 values.put(Calendars.CAL_SYNC3, "SYNC3:" + seedString); 172 values.put(Calendars.CAL_SYNC4, "SYNC4:" + seedString); 173 values.put(Calendars.CAL_SYNC5, "SYNC5:" + seedString); 174 values.put(Calendars.CAL_SYNC6, "SYNC6:" + seedString); 175 176 return values; 177 } 178 179 /** 180 * Creates a set of values with just the updates and modifies the 181 * original values to the expected values 182 */ 183 public static ContentValues getUpdateCalendarValuesWithOriginal( 184 ContentValues original, int seed) { 185 ContentValues values = new ContentValues(); 186 String seedString = Long.toString(seed); 187 188 values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString); 189 values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed); 190 values.put(Calendars.VISIBLE, seed % 2); 191 values.put(Calendars.SYNC_EVENTS, seed % 2); 192 193 original.putAll(values); 194 original.put(Calendars.DIRTY, 1); 195 196 return values; 197 } 198 199 public static int deleteCalendarById(ContentResolver resolver, long id) { 200 return resolver.delete(Calendars.CONTENT_URI, Calendars._ID + "=?", 201 new String[] { Long.toString(id) }); 202 } 203 204 public static int deleteCalendarByAccount(ContentResolver resolver, String account) { 205 return resolver.delete(Calendars.CONTENT_URI, Calendars.ACCOUNT_NAME + "=?", 206 new String[] { account }); 207 } 208 209 public static Cursor getCalendarsByAccount(ContentResolver resolver, String account) { 210 String selection = Calendars.ACCOUNT_TYPE + "=?"; 211 String[] selectionArgs; 212 if (account != null) { 213 selection += " AND " + Calendars.ACCOUNT_NAME + "=?"; 214 selectionArgs = new String[2]; 215 selectionArgs[1] = account; 216 } else { 217 selectionArgs = new String[1]; 218 } 219 selectionArgs[0] = CTS_TEST_TYPE; 220 221 return resolver.query(Calendars.CONTENT_URI, CALENDARS_SYNC_PROJECTION, selection, 222 selectionArgs, null); 223 } 224 } 225 226 /** 227 * Helper class for manipulating entries in the _sync_state table. 228 */ 229 private static class SyncStateHelper { 230 public static final String[] SYNCSTATE_PROJECTION = new String[] { 231 SyncState._ID, 232 SyncState.ACCOUNT_NAME, 233 SyncState.ACCOUNT_TYPE, 234 SyncState.DATA 235 }; 236 237 private static final byte[] SAMPLE_SYNC_DATA = { 238 (byte) 'H', (byte) 'e', (byte) 'l', (byte) 'l', (byte) 'o' 239 }; 240 241 private SyncStateHelper() {} // do not instantiate 242 243 /** 244 * Creates a new set of values for creating a new _sync_state entry. 245 */ 246 public static ContentValues getNewSyncStateValues(String account) { 247 ContentValues values = new ContentValues(); 248 values.put(SyncState.DATA, SAMPLE_SYNC_DATA); 249 values.put(SyncState.ACCOUNT_NAME, account); 250 values.put(SyncState.ACCOUNT_TYPE, CTS_TEST_TYPE); 251 return values; 252 } 253 254 /** 255 * Retrieves the _sync_state entry with the specified ID. 256 */ 257 public static Cursor getSyncStateById(ContentResolver resolver, long id) { 258 Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id); 259 return resolver.query(uri, SYNCSTATE_PROJECTION, null, null, null); 260 } 261 262 /** 263 * Retrieves the _sync_state entry for the specified account. 264 */ 265 public static Cursor getSyncStateByAccount(ContentResolver resolver, String account) { 266 assertNotNull(account); 267 String selection = SyncState.ACCOUNT_TYPE + "=? AND " + SyncState.ACCOUNT_NAME + "=?"; 268 String[] selectionArgs = new String[] { CTS_TEST_TYPE, account }; 269 270 return resolver.query(SyncState.CONTENT_URI, SYNCSTATE_PROJECTION, selection, 271 selectionArgs, null); 272 } 273 274 /** 275 * Deletes the _sync_state entry with the specified ID. Always done as app. 276 */ 277 public static int deleteSyncStateById(ContentResolver resolver, long id) { 278 Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id); 279 return resolver.delete(uri, null, null); 280 } 281 282 /** 283 * Deletes the _sync_state entry associated with the specified account. Can be done 284 * as app or sync adapter. 285 */ 286 public static int deleteSyncStateByAccount(ContentResolver resolver, String account, 287 boolean asSyncAdapter) { 288 Uri uri = SyncState.CONTENT_URI; 289 if (asSyncAdapter) { 290 uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); 291 } 292 return resolver.delete(uri, SyncState.ACCOUNT_NAME + "=?", 293 new String[] { account }); 294 } 295 } 296 297 // @formatter:off 298 private static class EventHelper { 299 public static final String[] EVENTS_PROJECTION = new String[] { 300 Events._ID, 301 Events.ACCOUNT_NAME, 302 Events.ACCOUNT_TYPE, 303 Events.OWNER_ACCOUNT, 304 // Events.ORGANIZER_CAN_RESPOND, from Calendars 305 // Events.CAN_CHANGE_TZ, from Calendars 306 // Events.MAX_REMINDERS, from Calendars 307 Events.CALENDAR_ID, 308 // Events.CALENDAR_DISPLAY_NAME, from Calendars 309 // Events.CALENDAR_COLOR, from Calendars 310 // Events.CALENDAR_ACL, from Calendars 311 // Events.CALENDAR_VISIBLE, from Calendars 312 Events.SYNC_DATA3, 313 Events.SYNC_DATA6, 314 Events.TITLE, 315 Events.EVENT_LOCATION, 316 Events.DESCRIPTION, 317 Events.STATUS, 318 Events.SELF_ATTENDEE_STATUS, 319 Events.DTSTART, 320 Events.DTEND, 321 Events.EVENT_TIMEZONE, 322 Events.EVENT_END_TIMEZONE, 323 Events.EVENT_COLOR, 324 Events.EVENT_COLOR_KEY, 325 Events.DURATION, 326 Events.ALL_DAY, 327 Events.ACCESS_LEVEL, 328 Events.AVAILABILITY, 329 Events.HAS_ALARM, 330 Events.HAS_EXTENDED_PROPERTIES, 331 Events.RRULE, 332 Events.RDATE, 333 Events.EXRULE, 334 Events.EXDATE, 335 Events.ORIGINAL_ID, 336 Events.ORIGINAL_SYNC_ID, 337 Events.ORIGINAL_INSTANCE_TIME, 338 Events.ORIGINAL_ALL_DAY, 339 Events.LAST_DATE, 340 Events.HAS_ATTENDEE_DATA, 341 Events.GUESTS_CAN_MODIFY, 342 Events.GUESTS_CAN_INVITE_OTHERS, 343 Events.GUESTS_CAN_SEE_GUESTS, 344 Events.ORGANIZER, 345 Events.DELETED, 346 Events._SYNC_ID, 347 Events.SYNC_DATA4, 348 Events.SYNC_DATA5, 349 Events.DIRTY, 350 Events.SYNC_DATA8, 351 Events.SYNC_DATA2, 352 Events.SYNC_DATA1, 353 Events.SYNC_DATA2, 354 Events.SYNC_DATA3, 355 Events.SYNC_DATA4, 356 }; 357 // @formatter:on 358 359 private EventHelper() {} // do not instantiate this class 360 361 /** 362 * Constructs a set of name/value pairs that can be used to create a Calendar event. 363 * Various fields are generated from the seed value. 364 */ 365 public static ContentValues getNewEventValues( 366 String account, int seed, long calendarId, boolean asSyncAdapter) { 367 String seedString = Long.toString(seed); 368 ContentValues values = new ContentValues(); 369 values.put(Events.ORGANIZER, "ORGANIZER:" + seedString); 370 371 values.put(Events.TITLE, "TITLE:" + seedString); 372 values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString); 373 374 values.put(Events.CALENDAR_ID, calendarId); 375 376 values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString); 377 values.put(Events.STATUS, seed % 2); // avoid STATUS_CANCELED for general testing 378 379 values.put(Events.DTSTART, seed); 380 values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS); 381 values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]); 382 values.put(Events.EVENT_COLOR, seed); 383 // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) % 384 // TIME_ZONES.length]); 385 if ((seed % 2) == 0) { 386 // Either set to zero, or leave unset to get default zero. 387 // Must be 0 or dtstart/dtend will get adjusted. 388 values.put(Events.ALL_DAY, 0); 389 } 390 values.put(Events.ACCESS_LEVEL, seed % 4); 391 values.put(Events.AVAILABILITY, seed % 2); 392 values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2); 393 values.put(Events.HAS_ATTENDEE_DATA, seed % 2); 394 values.put(Events.GUESTS_CAN_MODIFY, seed % 2); 395 values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2); 396 values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2); 397 398 // Default is STATUS_TENTATIVE (0). We either set it to that explicitly, or leave 399 // it set to the default. 400 if (seed != Events.STATUS_TENTATIVE) { 401 values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE); 402 } 403 404 if (asSyncAdapter) { 405 values.put(Events._SYNC_ID, "SYNC_ID:" + seedString); 406 values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString); 407 values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString); 408 values.put(Events.SYNC_DATA3, "HTML:" + seedString); 409 values.put(Events.SYNC_DATA6, "COMMENTS:" + seedString); 410 values.put(Events.DIRTY, 0); 411 values.put(Events.SYNC_DATA8, "0"); 412 } else { 413 // only the sync adapter can set the DIRTY flag 414 //values.put(Events.DIRTY, 1); 415 } 416 // values.put(Events.SYNC1, "SYNC1:" + seedString); 417 // values.put(Events.SYNC2, "SYNC2:" + seedString); 418 // values.put(Events.SYNC3, "SYNC3:" + seedString); 419 // values.put(Events.SYNC4, "SYNC4:" + seedString); 420 // values.put(Events.SYNC5, "SYNC5:" + seedString); 421 // Events.RRULE, 422 // Events.RDATE, 423 // Events.EXRULE, 424 // Events.EXDATE, 425 // // Events.ORIGINAL_ID 426 // Events.ORIGINAL_EVENT, // rename ORIGINAL_SYNC_ID 427 // Events.ORIGINAL_INSTANCE_TIME, 428 // Events.ORIGINAL_ALL_DAY, 429 430 return values; 431 } 432 433 /** 434 * Constructs a set of name/value pairs that can be used to create a recurring 435 * Calendar event. 436 * 437 * A duration of "P1D" is treated as an all-day event. 438 * 439 * @param startWhen Starting date/time in RFC 3339 format 440 * @param duration Event duration, in RFC 2445 duration format 441 * @param rrule Recurrence rule 442 * @return name/value pairs to use when creating event 443 */ 444 public static ContentValues getNewRecurringEventValues(String account, int seed, 445 long calendarId, boolean asSyncAdapter, String startWhen, String duration, 446 String rrule) { 447 448 // Set up some general stuff. 449 ContentValues values = getNewEventValues(account, seed, calendarId, asSyncAdapter); 450 451 // Replace the DTSTART field. 452 String timeZone = values.getAsString(Events.EVENT_TIMEZONE); 453 Time time = new Time(timeZone); 454 time.parse3339(startWhen); 455 values.put(Events.DTSTART, time.toMillis(false)); 456 457 // Add in the recurrence-specific fields, and drop DTEND. 458 values.put(Events.RRULE, rrule); 459 values.put(Events.DURATION, duration); 460 values.remove(Events.DTEND); 461 462 return values; 463 } 464 465 /** 466 * Constructs the basic name/value pairs required for an exception to a recurring event. 467 * 468 * @param instanceStartMillis The start time of the instance 469 * @return name/value pairs to use when creating event 470 */ 471 public static ContentValues getNewExceptionValues(long instanceStartMillis) { 472 ContentValues values = new ContentValues(); 473 values.put(Events.ORIGINAL_INSTANCE_TIME, instanceStartMillis); 474 475 return values; 476 } 477 478 public static ContentValues getUpdateEventValuesWithOriginal(ContentValues original, 479 int seed, boolean asSyncAdapter) { 480 String seedString = Long.toString(seed); 481 ContentValues values = new ContentValues(); 482 483 values.put(Events.TITLE, "TITLE:" + seedString); 484 values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString); 485 values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString); 486 values.put(Events.STATUS, seed % 3); 487 488 values.put(Events.DTSTART, seed); 489 values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS); 490 values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]); 491 // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) % 492 // TIME_ZONES.length]); 493 values.put(Events.ACCESS_LEVEL, seed % 4); 494 values.put(Events.AVAILABILITY, seed % 2); 495 values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2); 496 values.put(Events.HAS_ATTENDEE_DATA, seed % 2); 497 values.put(Events.GUESTS_CAN_MODIFY, seed % 2); 498 values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2); 499 values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2); 500 if (asSyncAdapter) { 501 values.put(Events._SYNC_ID, "SYNC_ID:" + seedString); 502 values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString); 503 values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString); 504 values.put(Events.DIRTY, 0); 505 } 506 original.putAll(values); 507 return values; 508 } 509 510 public static void addDefaultReadOnlyValues(ContentValues values, String account, 511 boolean asSyncAdapter) { 512 values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE); 513 values.put(Events.DELETED, 0); 514 values.put(Events.DIRTY, asSyncAdapter ? 0 : 1); 515 values.put(Events.OWNER_ACCOUNT, CalendarHelper.generateCalendarOwnerEmail(account)); 516 values.put(Events.ACCOUNT_TYPE, CTS_TEST_TYPE); 517 values.put(Events.ACCOUNT_NAME, account); 518 } 519 520 /** 521 * Generates a RFC2445-format duration string. 522 */ 523 private static String generateDurationString(long durationMillis, boolean isAllDay) { 524 long durationSeconds = durationMillis / 1000; 525 526 // The server may react differently to an all-day event specified as "P1D" than 527 // it will to "PT86400S"; see b/1594638. 528 if (isAllDay && (durationSeconds % 86400) == 0) { 529 return "P" + durationSeconds / 86400 + "D"; 530 } else { 531 return "PT" + durationSeconds + "S"; 532 } 533 } 534 535 /** 536 * Deletes the event, and updates the values. 537 * @param resolver The resolver to issue the query against. 538 * @param uri The deletion URI. 539 * @param values Set of values to update (sets DELETED and DIRTY). 540 * @return The number of rows modified. 541 */ 542 public static int deleteEvent(ContentResolver resolver, Uri uri, ContentValues values) { 543 values.put(Events.DELETED, 1); 544 values.put(Events.DIRTY, 1); 545 return resolver.delete(uri, null, null); 546 } 547 548 public static int deleteEventAsSyncAdapter(ContentResolver resolver, Uri uri, 549 String account) { 550 Uri syncUri = asSyncAdapter(uri, account, CTS_TEST_TYPE); 551 return resolver.delete(syncUri, null, null); 552 } 553 554 public static Cursor getEventsByAccount(ContentResolver resolver, String account) { 555 String selection = Calendars.ACCOUNT_TYPE + "=?"; 556 String[] selectionArgs; 557 if (account != null) { 558 selection += " AND " + Calendars.ACCOUNT_NAME + "=?"; 559 selectionArgs = new String[2]; 560 selectionArgs[1] = account; 561 } else { 562 selectionArgs = new String[1]; 563 } 564 selectionArgs[0] = CTS_TEST_TYPE; 565 return resolver.query(Events.CONTENT_URI, EVENTS_PROJECTION, selection, selectionArgs, 566 null); 567 } 568 569 public static Cursor getEventByUri(ContentResolver resolver, Uri uri) { 570 return resolver.query(uri, EVENTS_PROJECTION, null, null, null); 571 } 572 573 /** 574 * Looks up the specified Event in the database and returns the "selfAttendeeStatus" 575 * value. 576 */ 577 public static int lookupSelfAttendeeStatus(ContentResolver resolver, long eventId) { 578 return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId, 579 Events.SELF_ATTENDEE_STATUS); 580 } 581 582 /** 583 * Looks up the specified Event in the database and returns the "hasAlarm" 584 * value. 585 */ 586 public static int lookupHasAlarm(ContentResolver resolver, long eventId) { 587 return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId, 588 Events.HAS_ALARM); 589 } 590 } 591 592 /** 593 * Helper class for manipulating entries in the Attendees table. 594 */ 595 private static class AttendeeHelper { 596 public static final String[] ATTENDEES_PROJECTION = new String[] { 597 Attendees._ID, 598 Attendees.EVENT_ID, 599 Attendees.ATTENDEE_NAME, 600 Attendees.ATTENDEE_EMAIL, 601 Attendees.ATTENDEE_STATUS, 602 Attendees.ATTENDEE_RELATIONSHIP, 603 Attendees.ATTENDEE_TYPE 604 }; 605 // indexes into projection 606 public static final int ATTENDEES_ID_INDEX = 0; 607 public static final int ATTENDEES_EVENT_ID_INDEX = 1; 608 609 // do not instantiate 610 private AttendeeHelper() {} 611 612 /** 613 * Adds a new attendee to the specified event. 614 * 615 * @return the _id of the new attendee, or -1 on failure 616 */ 617 public static long addAttendee(ContentResolver resolver, long eventId, String name, 618 String email, int status, int relationship, int type) { 619 Uri uri = Attendees.CONTENT_URI; 620 621 ContentValues attendee = new ContentValues(); 622 attendee.put(Attendees.EVENT_ID, eventId); 623 attendee.put(Attendees.ATTENDEE_NAME, name); 624 attendee.put(Attendees.ATTENDEE_EMAIL, email); 625 attendee.put(Attendees.ATTENDEE_STATUS, status); 626 attendee.put(Attendees.ATTENDEE_RELATIONSHIP, relationship); 627 attendee.put(Attendees.ATTENDEE_TYPE, type); 628 Uri result = resolver.insert(uri, attendee); 629 return ContentUris.parseId(result); 630 } 631 632 /** 633 * Finds all Attendees rows for the specified event and email address. The returned 634 * cursor will use {@link AttendeeHelper#ATTENDEES_PROJECTION}. 635 */ 636 public static Cursor findAttendeesByEmail(ContentResolver resolver, long eventId, 637 String email) { 638 return resolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION, 639 Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_EMAIL + "=?", 640 new String[] { String.valueOf(eventId), email }, null); 641 } 642 } 643 644 /** 645 * Helper class for manipulating entries in the Colors table. 646 */ 647 private static class ColorHelper { 648 public static final String WHERE_COLOR_ACCOUNT = Colors.ACCOUNT_NAME + "=? AND " 649 + Colors.ACCOUNT_TYPE + "=?"; 650 public static final String WHERE_COLOR_ACCOUNT_AND_INDEX = WHERE_COLOR_ACCOUNT + " AND " 651 + Colors.COLOR_KEY + "=?"; 652 653 public static final String[] COLORS_PROJECTION = new String[] { 654 Colors._ID, // 0 655 Colors.ACCOUNT_NAME, // 1 656 Colors.ACCOUNT_TYPE, // 2 657 Colors.DATA, // 3 658 Colors.COLOR_TYPE, // 4 659 Colors.COLOR_KEY, // 5 660 Colors.COLOR, // 6 661 }; 662 // indexes into projection 663 public static final int COLORS_ID_INDEX = 0; 664 public static final int COLORS_INDEX_INDEX = 5; 665 public static final int COLORS_COLOR_INDEX = 6; 666 667 public static final int[] DEFAULT_TYPES = new int[] { 668 Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR, 669 Colors.TYPE_CALENDAR, Colors.TYPE_EVENT, Colors.TYPE_EVENT, Colors.TYPE_EVENT, 670 Colors.TYPE_EVENT, 671 }; 672 public static final int[] DEFAULT_COLORS = new int[] { 673 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFAA00AA, 0xFF00AAAA, 0xFF333333, 0xFFAAAA00, 674 0xFFAAAAAA, 675 }; 676 public static final String[] DEFAULT_INDICES = new String[] { 677 "000", "001", "010", "011", "100", "101", "110", "111", 678 }; 679 680 public static final int C_COLOR_0 = 0; 681 public static final int C_COLOR_1 = 1; 682 public static final int C_COLOR_2 = 2; 683 public static final int C_COLOR_3 = 3; 684 public static final int E_COLOR_0 = 4; 685 public static final int E_COLOR_1 = 5; 686 public static final int E_COLOR_2 = 6; 687 public static final int E_COLOR_3 = 7; 688 689 // do not instantiate 690 private ColorHelper() { 691 } 692 693 /** 694 * Adds a new color to the colors table. 695 * 696 * @return the _id of the new color, or -1 on failure 697 */ 698 public static long addColor(ContentResolver resolver, String accountName, 699 String accountType, String data, String index, int type, int color) { 700 Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType); 701 702 ContentValues colorValues = new ContentValues(); 703 colorValues.put(Colors.DATA, data); 704 colorValues.put(Colors.COLOR_KEY, index); 705 colorValues.put(Colors.COLOR_TYPE, type); 706 colorValues.put(Colors.COLOR, color); 707 Uri result = resolver.insert(uri, colorValues); 708 return ContentUris.parseId(result); 709 } 710 711 /** 712 * Finds the color specified by an account name/type and a color index. 713 * The returned cursor will use {@link ColorHelper#COLORS_PROJECTION}. 714 */ 715 public static Cursor findColorByIndex(ContentResolver resolver, String accountName, 716 String accountType, String index) { 717 return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION, 718 WHERE_COLOR_ACCOUNT_AND_INDEX, 719 new String[] {accountName, accountType, index}, null); 720 } 721 722 public static Cursor findColorsByAccount(ContentResolver resolver, String accountName, 723 String accountType) { 724 return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION, WHERE_COLOR_ACCOUNT, 725 new String[] { accountName, accountType }, null); 726 } 727 728 /** 729 * Adds a default set of test colors to the Colors table under the given 730 * account. 731 * 732 * @return true if the default colors were added successfully 733 */ 734 public static boolean addDefaultColorsToAccount(ContentResolver resolver, 735 String accountName, String accountType) { 736 for (int i = 0; i < DEFAULT_INDICES.length; i++) { 737 long id = addColor(resolver, accountName, accountType, null, DEFAULT_INDICES[i], 738 DEFAULT_TYPES[i], DEFAULT_COLORS[i]); 739 if (id == -1) { 740 return false; 741 } 742 } 743 return true; 744 } 745 746 public static void deleteColorsByAccount(ContentResolver resolver, String accountName, 747 String accountType) { 748 Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType); 749 resolver.delete(uri, WHERE_COLOR_ACCOUNT, new String[] { accountName, accountType }); 750 } 751 } 752 753 754 /** 755 * Helper class for manipulating entries in the Reminders table. 756 */ 757 private static class ReminderHelper { 758 public static final String[] REMINDERS_PROJECTION = new String[] { 759 Reminders._ID, 760 Reminders.EVENT_ID, 761 Reminders.MINUTES, 762 Reminders.METHOD 763 }; 764 // indexes into projection 765 public static final int REMINDERS_ID_INDEX = 0; 766 public static final int REMINDERS_EVENT_ID_INDEX = 1; 767 public static final int REMINDERS_MINUTES_INDEX = 2; 768 public static final int REMINDERS_METHOD_INDEX = 3; 769 770 // do not instantiate 771 private ReminderHelper() {} 772 773 /** 774 * Adds a new reminder to the specified event. 775 * 776 * @return the _id of the new reminder, or -1 on failure 777 */ 778 public static long addReminder(ContentResolver resolver, long eventId, int minutes, 779 int method) { 780 Uri uri = Reminders.CONTENT_URI; 781 782 ContentValues reminder = new ContentValues(); 783 reminder.put(Reminders.EVENT_ID, eventId); 784 reminder.put(Reminders.MINUTES, minutes); 785 reminder.put(Reminders.METHOD, method); 786 Uri result = resolver.insert(uri, reminder); 787 return ContentUris.parseId(result); 788 } 789 790 /** 791 * Finds all Reminders rows for the specified event. The returned cursor will use 792 * {@link ReminderHelper#REMINDERS_PROJECTION}. 793 */ 794 public static Cursor findRemindersByEventId(ContentResolver resolver, long eventId) { 795 return resolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, 796 Reminders.EVENT_ID + "=?", new String[] { String.valueOf(eventId) }, null); 797 } 798 799 /** 800 * Looks up the specified Reminders row and returns the "method" value. 801 */ 802 public static int lookupMethod(ContentResolver resolver, long remId) { 803 return getIntFromDatabase(resolver, Reminders.CONTENT_URI, remId, 804 Reminders.METHOD); 805 } 806 } 807 808 /** 809 * Helper class for manipulating entries in the ExtendedProperties table. 810 */ 811 private static class ExtendedPropertiesHelper { 812 public static final String[] EXTENDED_PROPERTIES_PROJECTION = new String[] { 813 ExtendedProperties._ID, 814 ExtendedProperties.EVENT_ID, 815 ExtendedProperties.NAME, 816 ExtendedProperties.VALUE 817 }; 818 // indexes into projection 819 public static final int EXTENDED_PROPERTIES_ID_INDEX = 0; 820 public static final int EXTENDED_PROPERTIES_EVENT_ID_INDEX = 1; 821 public static final int EXTENDED_PROPERTIES_NAME_INDEX = 2; 822 public static final int EXTENDED_PROPERTIES_VALUE_INDEX = 3; 823 824 // do not instantiate 825 private ExtendedPropertiesHelper() {} 826 827 /** 828 * Adds a new ExtendedProperty for the specified event. Runs as sync adapter. 829 * 830 * @return the _id of the new ExtendedProperty, or -1 on failure 831 */ 832 public static long addExtendedProperty(ContentResolver resolver, String account, 833 long eventId, String name, String value) { 834 Uri uri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE); 835 836 ContentValues ep = new ContentValues(); 837 ep.put(ExtendedProperties.EVENT_ID, eventId); 838 ep.put(ExtendedProperties.NAME, name); 839 ep.put(ExtendedProperties.VALUE, value); 840 Uri result = resolver.insert(uri, ep); 841 return ContentUris.parseId(result); 842 } 843 844 /** 845 * Finds all ExtendedProperties rows for the specified event. The returned cursor will 846 * use {@link ExtendedPropertiesHelper#EXTENDED_PROPERTIES_PROJECTION}. 847 */ 848 public static Cursor findExtendedPropertiesByEventId(ContentResolver resolver, 849 long eventId) { 850 return resolver.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROPERTIES_PROJECTION, 851 ExtendedProperties.EVENT_ID + "=?", 852 new String[] { String.valueOf(eventId) }, null); 853 } 854 855 /** 856 * Finds an ExtendedProperties entry with a matching name for the specified event, and 857 * returns the value. Throws an exception if we don't find exactly one row. 858 */ 859 public static String lookupValueByName(ContentResolver resolver, long eventId, 860 String name) { 861 Cursor cursor = resolver.query(ExtendedProperties.CONTENT_URI, 862 EXTENDED_PROPERTIES_PROJECTION, 863 ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?", 864 new String[] { String.valueOf(eventId), name }, null); 865 866 try { 867 if (cursor.getCount() != 1) { 868 throw new RuntimeException("Got " + cursor.getCount() + " results, expected 1"); 869 } 870 871 cursor.moveToFirst(); 872 return cursor.getString(EXTENDED_PROPERTIES_VALUE_INDEX); 873 } finally { 874 if (cursor != null) { 875 cursor.close(); 876 } 877 } 878 } 879 } 880 881 /** 882 * Creates an updated URI that includes query parameters that identify the source as a 883 * sync adapter. 884 */ 885 static Uri asSyncAdapter(Uri uri, String account, String accountType) { 886 return uri.buildUpon() 887 .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, 888 "true") 889 .appendQueryParameter(Calendars.ACCOUNT_NAME, account) 890 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); 891 } 892 893 /** 894 * Returns the value of the specified row and column in the Events table, as an integer. 895 * Throws an exception if the specified row or column doesn't exist or doesn't contain 896 * an integer (e.g. null entry). 897 */ 898 private static int getIntFromDatabase(ContentResolver resolver, Uri uri, long rowId, 899 String columnName) { 900 String[] projection = { columnName }; 901 String selection = SQL_WHERE_ID; 902 String[] selectionArgs = { String.valueOf(rowId) }; 903 904 Cursor c = resolver.query(uri, projection, selection, selectionArgs, null); 905 try { 906 assertEquals(1, c.getCount()); 907 c.moveToFirst(); 908 return c.getInt(0); 909 } finally { 910 c.close(); 911 } 912 } 913 914 @Override 915 protected void setUp() throws Exception { 916 super.setUp(); 917 mContentResolver = getInstrumentation().getTargetContext().getContentResolver(); 918 } 919 920 @MediumTest 921 public void testCalendarCreationAndDeletion() { 922 String account = "cc1_account"; 923 int seed = 0; 924 925 // Clean up just in case 926 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 927 long id = createAndVerifyCalendar(account, seed++, null); 928 929 removeAndVerifyCalendar(account, id); 930 } 931 932 /** 933 * Tests whether the default projections work. We don't need to have any data in 934 * the calendar, since it's testing the database schema. 935 */ 936 @MediumTest 937 public void testDefaultProjections() { 938 String account = "dproj_account"; 939 int seed = 0; 940 941 // Clean up just in case 942 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 943 long id = createAndVerifyCalendar(account, seed++, null); 944 945 Cursor c; 946 Uri uri; 947 // Calendars 948 c = mContentResolver.query(Calendars.CONTENT_URI, null, null, null, null); 949 c.close(); 950 // Events 951 c = mContentResolver.query(Events.CONTENT_URI, null, null, null, null); 952 c.close(); 953 // Instances 954 uri = Uri.withAppendedPath(Instances.CONTENT_URI, "0/1"); 955 c = mContentResolver.query(uri, null, null, null, null); 956 c.close(); 957 // Attendees 958 c = mContentResolver.query(Attendees.CONTENT_URI, null, null, null, null); 959 c.close(); 960 // Reminders (only REMINDERS_ID currently uses default projection) 961 uri = ContentUris.withAppendedId(Reminders.CONTENT_URI, 0); 962 c = mContentResolver.query(uri, null, null, null, null); 963 c.close(); 964 // CalendarAlerts 965 c = mContentResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI, 966 null, null, null, null); 967 c.close(); 968 // CalendarCache 969 c = mContentResolver.query(CalendarContract.CalendarCache.URI, 970 null, null, null, null); 971 c.close(); 972 // CalendarEntity 973 c = mContentResolver.query(CalendarContract.CalendarEntity.CONTENT_URI, 974 null, null, null, null); 975 c.close(); 976 // EventEntities 977 c = mContentResolver.query(CalendarContract.EventsEntity.CONTENT_URI, 978 null, null, null, null); 979 c.close(); 980 // EventDays 981 uri = Uri.withAppendedPath(CalendarContract.EventDays.CONTENT_URI, "1/2"); 982 c = mContentResolver.query(uri, null, null, null, null); 983 c.close(); 984 // ExtendedProperties 985 c = mContentResolver.query(CalendarContract.ExtendedProperties.CONTENT_URI, 986 null, null, null, null); 987 c.close(); 988 989 removeAndVerifyCalendar(account, id); 990 } 991 992 /** 993 * Exercises the EventsEntity class. 994 */ 995 @MediumTest 996 public void testEventsEntityQuery() { 997 String account = "eeq_account"; 998 int seed = 0; 999 1000 // Clean up just in case. 1001 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1002 1003 // Create calendar. 1004 long calendarId = createAndVerifyCalendar(account, seed++, null); 1005 1006 // Create three events. We need to make sure SELF_ATTENDEE_STATUS isn't set, because 1007 // that causes the provider to generate an Attendees entry, and that'll throw off 1008 // our expected count. 1009 ContentValues eventValues; 1010 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1011 eventValues.remove(Events.SELF_ATTENDEE_STATUS); 1012 long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1013 assertTrue(eventId1 >= 0); 1014 1015 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1016 eventValues.remove(Events.SELF_ATTENDEE_STATUS); 1017 long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1018 assertTrue(eventId2 >= 0); 1019 1020 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1021 eventValues.remove(Events.SELF_ATTENDEE_STATUS); 1022 long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1023 assertTrue(eventId3 >= 0); 1024 1025 /* 1026 * Add some attendees, reminders, and extended properties. 1027 */ 1028 Uri uri, syncUri; 1029 1030 syncUri = asSyncAdapter(Reminders.CONTENT_URI, account, CTS_TEST_TYPE); 1031 ContentValues remValues = new ContentValues(); 1032 remValues.put(Reminders.EVENT_ID, eventId1); 1033 remValues.put(Reminders.MINUTES, 10); 1034 remValues.put(Reminders.METHOD, Reminders.METHOD_ALERT); 1035 mContentResolver.insert(syncUri, remValues); 1036 remValues.put(Reminders.MINUTES, 20); 1037 mContentResolver.insert(syncUri, remValues); 1038 1039 syncUri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE); 1040 ContentValues extended = new ContentValues(); 1041 extended.put(ExtendedProperties.NAME, "foo"); 1042 extended.put(ExtendedProperties.VALUE, "bar"); 1043 extended.put(ExtendedProperties.EVENT_ID, eventId2); 1044 mContentResolver.insert(syncUri, extended); 1045 extended.put(ExtendedProperties.EVENT_ID, eventId1); 1046 mContentResolver.insert(syncUri, extended); 1047 extended.put(ExtendedProperties.NAME, "foo2"); 1048 extended.put(ExtendedProperties.VALUE, "bar2"); 1049 mContentResolver.insert(syncUri, extended); 1050 1051 syncUri = asSyncAdapter(Attendees.CONTENT_URI, account, CTS_TEST_TYPE); 1052 ContentValues attendee = new ContentValues(); 1053 attendee.put(Attendees.ATTENDEE_NAME, "Joe"); 1054 attendee.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account)); 1055 attendee.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED); 1056 attendee.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED); 1057 attendee.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_PERFORMER); 1058 attendee.put(Attendees.EVENT_ID, eventId3); 1059 mContentResolver.insert(syncUri, attendee); 1060 1061 /* 1062 * Iterate over all events on our calendar. Peek at a few values to see if they 1063 * look reasonable. 1064 */ 1065 EntityIterator ei = EventsEntity.newEntityIterator( 1066 mContentResolver.query(EventsEntity.CONTENT_URI, null, Events.CALENDAR_ID + "=?", 1067 new String[] { String.valueOf(calendarId) }, null), 1068 mContentResolver); 1069 int count = 0; 1070 try { 1071 while (ei.hasNext()) { 1072 Entity entity = ei.next(); 1073 ContentValues values = entity.getEntityValues(); 1074 ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues(); 1075 long eventId = values.getAsLong(Events._ID); 1076 if (eventId == eventId1) { 1077 // 2 x reminder, 2 x extended properties 1078 assertEquals(4, subvalues.size()); 1079 } else if (eventId == eventId2) { 1080 // Extended properties 1081 assertEquals(1, subvalues.size()); 1082 ContentValues subContentValues = subvalues.get(0).values; 1083 String name = subContentValues.getAsString( 1084 CalendarContract.ExtendedProperties.NAME); 1085 String value = subContentValues.getAsString( 1086 CalendarContract.ExtendedProperties.VALUE); 1087 assertEquals("foo", name); 1088 assertEquals("bar", value); 1089 } else if (eventId == eventId3) { 1090 // Attendees 1091 assertEquals(1, subvalues.size()); 1092 } else { 1093 fail("should not be here"); 1094 } 1095 count++; 1096 } 1097 assertEquals(3, count); 1098 } finally { 1099 ei.close(); 1100 } 1101 1102 // Confirm that querying for a single event yields a single event. 1103 ei = EventsEntity.newEntityIterator( 1104 mContentResolver.query(EventsEntity.CONTENT_URI, null, SQL_WHERE_ID, 1105 new String[] { String.valueOf(eventId3) }, null), 1106 mContentResolver); 1107 try { 1108 count = 0; 1109 while (ei.hasNext()) { 1110 Entity entity = ei.next(); 1111 count++; 1112 } 1113 assertEquals(1, count); 1114 } finally { 1115 ei.close(); 1116 } 1117 1118 1119 removeAndVerifyCalendar(account, calendarId); 1120 } 1121 1122 /** 1123 * Exercises the CalendarEntity class. 1124 */ 1125 @MediumTest 1126 public void testCalendarEntityQuery() { 1127 String account1 = "ceq1_account"; 1128 String account2 = "ceq2_account"; 1129 String account3 = "ceq3_account"; 1130 int seed = 0; 1131 1132 // Clean up just in case. 1133 CalendarHelper.deleteCalendarByAccount(mContentResolver, account1); 1134 CalendarHelper.deleteCalendarByAccount(mContentResolver, account2); 1135 CalendarHelper.deleteCalendarByAccount(mContentResolver, account3); 1136 1137 // Create calendars. 1138 long calendarId1 = createAndVerifyCalendar(account1, seed++, null); 1139 long calendarId2 = createAndVerifyCalendar(account2, seed++, null); 1140 long calendarId3 = createAndVerifyCalendar(account3, seed++, null); 1141 1142 EntityIterator ei = CalendarEntity.newEntityIterator( 1143 mContentResolver.query(CalendarEntity.CONTENT_URI, null, 1144 Calendars._ID + "=? OR " + Calendars._ID + "=? OR " + Calendars._ID + "=?", 1145 new String[] { String.valueOf(calendarId1), String.valueOf(calendarId2), 1146 String.valueOf(calendarId3) }, 1147 null)); 1148 1149 try { 1150 int count = 0; 1151 while (ei.hasNext()) { 1152 Entity entity = ei.next(); 1153 count++; 1154 } 1155 assertEquals(3, count); 1156 } finally { 1157 ei.close(); 1158 } 1159 1160 removeAndVerifyCalendar(account1, calendarId1); 1161 removeAndVerifyCalendar(account2, calendarId2); 1162 removeAndVerifyCalendar(account3, calendarId3); 1163 } 1164 1165 /** 1166 * Tests creation and manipulation of Attendees. 1167 */ 1168 @MediumTest 1169 public void testAttendees() { 1170 String account = "att_account"; 1171 int seed = 0; 1172 1173 // Clean up just in case. 1174 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1175 1176 // Create calendar. 1177 long calendarId = createAndVerifyCalendar(account, seed++, null); 1178 1179 // Create two events, one with a value set for SELF_ATTENDEE_STATUS, one without. 1180 ContentValues eventValues; 1181 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1182 eventValues.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE); 1183 long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1184 assertTrue(eventId1 >= 0); 1185 1186 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1187 eventValues.remove(Events.SELF_ATTENDEE_STATUS); 1188 long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1189 assertTrue(eventId2 >= 0); 1190 1191 /* 1192 * Add some attendees to the first event. 1193 */ 1194 long attId1 = AttendeeHelper.addAttendee(mContentResolver, eventId1, 1195 "Alice", 1196 "alice (at) example.com", 1197 Attendees.ATTENDEE_STATUS_TENTATIVE, 1198 Attendees.RELATIONSHIP_ATTENDEE, 1199 Attendees.TYPE_REQUIRED); 1200 long attId2 = AttendeeHelper.addAttendee(mContentResolver, eventId1, 1201 "Betty", 1202 "betty (at) example.com", 1203 Attendees.ATTENDEE_STATUS_DECLINED, 1204 Attendees.RELATIONSHIP_ATTENDEE, 1205 Attendees.TYPE_NONE); 1206 long attId3 = AttendeeHelper.addAttendee(mContentResolver, eventId1, 1207 "Carol", 1208 "carol (at) example.com", 1209 Attendees.ATTENDEE_STATUS_DECLINED, 1210 Attendees.RELATIONSHIP_ATTENDEE, 1211 Attendees.TYPE_OPTIONAL); 1212 1213 /* 1214 * Find the event1 "self" attendee entry. 1215 */ 1216 Cursor cursor = AttendeeHelper.findAttendeesByEmail(mContentResolver, eventId1, 1217 CalendarHelper.generateCalendarOwnerEmail(account)); 1218 try { 1219 assertEquals(1, cursor.getCount()); 1220 //DatabaseUtils.dumpCursor(cursor); 1221 1222 cursor.moveToFirst(); 1223 long id = cursor.getLong(AttendeeHelper.ATTENDEES_ID_INDEX); 1224 1225 /* 1226 * Update the status field. The provider should automatically propagate the result. 1227 */ 1228 ContentValues update = new ContentValues(); 1229 Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, id); 1230 1231 update.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED); 1232 int count = mContentResolver.update(uri, update, null, null); 1233 assertEquals(1, count); 1234 1235 int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId1); 1236 assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status); 1237 1238 } finally { 1239 if (cursor != null) { 1240 cursor.close(); 1241 } 1242 } 1243 1244 /* 1245 * Do a bulk update of all Attendees for this event, changing any Attendee with status 1246 * "declined" to "invited". 1247 */ 1248 ContentValues bulkUpdate = new ContentValues(); 1249 bulkUpdate.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_INVITED); 1250 1251 int count = mContentResolver.update(Attendees.CONTENT_URI, bulkUpdate, 1252 Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_STATUS + "=?", 1253 new String[] { 1254 String.valueOf(eventId1), String.valueOf(Attendees.ATTENDEE_STATUS_DECLINED) 1255 }); 1256 assertEquals(2, count); 1257 1258 /* 1259 * Add a new, non-self attendee to the second event. 1260 */ 1261 long attId4 = AttendeeHelper.addAttendee(mContentResolver, eventId2, 1262 "Diana", 1263 "diana (at) example.com", 1264 Attendees.ATTENDEE_STATUS_ACCEPTED, 1265 Attendees.RELATIONSHIP_ATTENDEE, 1266 Attendees.TYPE_REQUIRED); 1267 1268 /* 1269 * Confirm that the selfAttendeeStatus on the second event has the default value. 1270 */ 1271 int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2); 1272 assertEquals(Attendees.ATTENDEE_STATUS_NONE, status); 1273 1274 /* 1275 * Create a new "self" attendee in the second event by updating the email address to 1276 * match that of the calendar owner. 1277 */ 1278 ContentValues newSelf = new ContentValues(); 1279 newSelf.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account)); 1280 count = mContentResolver.update(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4), 1281 newSelf, null, null); 1282 assertEquals(1, count); 1283 1284 /* 1285 * Confirm that the event's selfAttendeeStatus has been updated. 1286 */ 1287 status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2); 1288 assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status); 1289 1290 /* 1291 * TODO: (these are unexpected usage patterns) 1292 * - Update an Attendee's status and event_id to move it to a different event, and 1293 * confirm that the selfAttendeeStatus in the destination event is updated (rather 1294 * than that of the source event). 1295 * - Create two Attendees with email addresses that match "self" but have different 1296 * values for "status". Delete one and confirm that selfAttendeeStatus is changed 1297 * to that of the remaining Attendee. (There is no defined behavior for 1298 * selfAttendeeStatus when there are multiple matching Attendees.) 1299 */ 1300 1301 /* 1302 * Test deletion, singly by ID and in bulk. 1303 */ 1304 count = mContentResolver.delete(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4), 1305 null, null); 1306 assertEquals(1, count); 1307 1308 count = mContentResolver.delete(Attendees.CONTENT_URI, Attendees.EVENT_ID + "=?", 1309 new String[] { String.valueOf(eventId1) }); 1310 assertEquals(4, count); // 3 we created + 1 auto-added by the provider 1311 1312 removeAndVerifyCalendar(account, calendarId); 1313 } 1314 1315 /** 1316 * Tests creation and manipulation of Reminders. 1317 */ 1318 @MediumTest 1319 public void testReminders() { 1320 String account = "rem_account"; 1321 int seed = 0; 1322 1323 // Clean up just in case. 1324 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1325 1326 // Create calendar. 1327 long calendarId = createAndVerifyCalendar(account, seed++, null); 1328 1329 // Create events. 1330 ContentValues eventValues; 1331 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1332 long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1333 assertTrue(eventId1 >= 0); 1334 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1335 long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1336 assertTrue(eventId2 >= 0); 1337 1338 // No reminders, hasAlarm should be zero. 1339 int hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); 1340 assertEquals(0, hasAlarm); 1341 hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2); 1342 assertEquals(0, hasAlarm); 1343 1344 /* 1345 * Add some reminders. 1346 */ 1347 long remId1 = ReminderHelper.addReminder(mContentResolver, eventId1, 1348 10, Reminders.METHOD_DEFAULT); 1349 long remId2 = ReminderHelper.addReminder(mContentResolver, eventId1, 1350 15, Reminders.METHOD_ALERT); 1351 long remId3 = ReminderHelper.addReminder(mContentResolver, eventId1, 1352 20, Reminders.METHOD_SMS); // SMS isn't allowed for this calendar 1353 1354 // Should have been set to 1 by provider. 1355 hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); 1356 assertEquals(1, hasAlarm); 1357 1358 // Add a reminder to event2. 1359 ReminderHelper.addReminder(mContentResolver, eventId2, 1360 20, Reminders.METHOD_DEFAULT); 1361 hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2); 1362 assertEquals(1, hasAlarm); 1363 1364 1365 /* 1366 * Check the entries. 1367 */ 1368 Cursor cursor = ReminderHelper.findRemindersByEventId(mContentResolver, eventId1); 1369 try { 1370 assertEquals(3, cursor.getCount()); 1371 //DatabaseUtils.dumpCursor(cursor); 1372 1373 while (cursor.moveToNext()) { 1374 int minutes = cursor.getInt(ReminderHelper.REMINDERS_MINUTES_INDEX); 1375 int method = cursor.getInt(ReminderHelper.REMINDERS_METHOD_INDEX); 1376 switch (minutes) { 1377 case 10: 1378 assertEquals(Reminders.METHOD_DEFAULT, method); 1379 break; 1380 case 15: 1381 assertEquals(Reminders.METHOD_ALERT, method); 1382 break; 1383 case 20: 1384 assertEquals(Reminders.METHOD_SMS, method); 1385 break; 1386 default: 1387 fail("unexpected minutes " + minutes); 1388 break; 1389 } 1390 } 1391 } finally { 1392 if (cursor != null) { 1393 cursor.close(); 1394 } 1395 } 1396 1397 /* 1398 * Use the bulk update feature to change all METHOD_DEFAULT to METHOD_EMAIL. To make 1399 * this more interesting we first change remId3 to METHOD_DEFAULT. 1400 */ 1401 int count; 1402 ContentValues newValues = new ContentValues(); 1403 newValues.put(Reminders.METHOD, Reminders.METHOD_DEFAULT); 1404 count = mContentResolver.update(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId3), 1405 newValues, null, null); 1406 assertEquals(1, count); 1407 1408 newValues.put(Reminders.METHOD, Reminders.METHOD_EMAIL); 1409 count = mContentResolver.update(Reminders.CONTENT_URI, newValues, 1410 Reminders.EVENT_ID + "=? AND " + Reminders.METHOD + "=?", 1411 new String[] { 1412 String.valueOf(eventId1), String.valueOf(Reminders.METHOD_DEFAULT) 1413 }); 1414 assertEquals(2, count); 1415 1416 // check it 1417 int method = ReminderHelper.lookupMethod(mContentResolver, remId3); 1418 assertEquals(Reminders.METHOD_EMAIL, method); 1419 1420 /* 1421 * Delete some / all reminders and confirm that hasAlarm tracks it. 1422 * 1423 * You can also remove reminders from an event by updating the event_id column, but 1424 * that's defined as producing undefined behavior, so we don't do it here. 1425 */ 1426 count = mContentResolver.delete(Reminders.CONTENT_URI, 1427 Reminders.EVENT_ID + "=? AND " + Reminders.MINUTES + ">=?", 1428 new String[] { String.valueOf(eventId1), "15" }); 1429 assertEquals(2, count); 1430 hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); 1431 assertEquals(1, hasAlarm); 1432 1433 // Delete all reminders from both events. 1434 count = mContentResolver.delete(Reminders.CONTENT_URI, 1435 Reminders.EVENT_ID + "=? OR " + Reminders.EVENT_ID + "=?", 1436 new String[] { String.valueOf(eventId1), String.valueOf(eventId2) }); 1437 assertEquals(2, count); 1438 hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); 1439 assertEquals(0, hasAlarm); 1440 hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2); 1441 assertEquals(0, hasAlarm); 1442 1443 /* 1444 * Add a couple of reminders and then delete one with the by-ID URI. 1445 */ 1446 long remId4 = ReminderHelper.addReminder(mContentResolver, eventId1, 1447 10, Reminders.METHOD_EMAIL); 1448 long remId5 = ReminderHelper.addReminder(mContentResolver, eventId1, 1449 15, Reminders.METHOD_EMAIL); 1450 count = mContentResolver.delete(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId4), 1451 null, null); 1452 assertEquals(1, count); 1453 1454 removeAndVerifyCalendar(account, calendarId); 1455 } 1456 1457 /** 1458 * A listener for the EVENT_REMINDER broadcast that is expected to be fired by the 1459 * provider at the reminder time. 1460 */ 1461 public class MockReminderReceiver extends BroadcastReceiver { 1462 public boolean received = false; 1463 1464 @Override 1465 public void onReceive(Context context, Intent intent) { 1466 final String action = intent.getAction(); 1467 if (action.equals(CalendarContract.ACTION_EVENT_REMINDER)) { 1468 received = true; 1469 } 1470 } 1471 } 1472 1473 /** 1474 * Test that reminders result in the expected broadcast at reminder time. 1475 */ 1476 public void testRemindersAlarm() throws Exception { 1477 // Setup: register a mock listener for the broadcast we expect to fire at the 1478 // reminder time. 1479 final MockReminderReceiver reminderReceiver = new MockReminderReceiver(); 1480 IntentFilter filter = new IntentFilter(CalendarContract.ACTION_EVENT_REMINDER); 1481 filter.addDataScheme("content"); 1482 getInstrumentation().getTargetContext().registerReceiver(reminderReceiver, filter); 1483 1484 // Clean up just in case. 1485 String account = "rem_account"; 1486 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1487 1488 // Create calendar. Use '1' as seed as this sets the VISIBLE field to 1. 1489 // The calendar must be visible for its notifications to occur. 1490 long calendarId = createAndVerifyCalendar(account, 1, null); 1491 1492 // Create event for 15 min in the past, with a 10 min reminder, so that it will 1493 // trigger immediately. 1494 ContentValues eventValues; 1495 int seed = 0; 1496 long now = System.currentTimeMillis(); 1497 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1498 eventValues.put(Events.DTSTART, now - DateUtils.MINUTE_IN_MILLIS * 15); 1499 eventValues.put(Events.DTEND, now + DateUtils.HOUR_IN_MILLIS); 1500 long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1501 assertTrue(eventId >= 0); 1502 ReminderHelper.addReminder(mContentResolver, eventId, 10, Reminders.METHOD_ALERT); 1503 1504 // Confirm that the EVENT_REMINDER broadcast was fired by the provider. 1505 new PollingCheck(POLLING_TIMEOUT) { 1506 @Override 1507 protected boolean check() { 1508 return reminderReceiver.received; 1509 } 1510 }.run(); 1511 assertTrue(reminderReceiver.received); 1512 1513 removeAndVerifyCalendar(account, calendarId); 1514 } 1515 1516 @MediumTest 1517 public void testColorWriteRequirements() { 1518 String account = "colw_account"; 1519 String account2 = "colw2_account"; 1520 int seed = 0; 1521 Uri uri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE); 1522 Uri uri2 = asSyncAdapter(Colors.CONTENT_URI, account2, CTS_TEST_TYPE); 1523 1524 // Clean up just in case 1525 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1526 ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); 1527 1528 ContentValues colorValues = new ContentValues(); 1529 // Account name/type must be in the query params, so may be left 1530 // out here 1531 colorValues.put(Colors.DATA, "0"); 1532 colorValues.put(Colors.COLOR_KEY, "1"); 1533 colorValues.put(Colors.COLOR_TYPE, 0); 1534 colorValues.put(Colors.COLOR, 0xff000000); 1535 1536 // Verify only a sync adapter can write to Colors 1537 try { 1538 mContentResolver.insert(Colors.CONTENT_URI, colorValues); 1539 fail("Should not allow non-sync adapter to insert colors"); 1540 } catch (IllegalArgumentException e) { 1541 // WAI 1542 } 1543 1544 // Verify everything except DATA is required 1545 ContentValues testVals = new ContentValues(colorValues); 1546 for (String key : colorValues.keySet()) { 1547 1548 testVals.remove(key); 1549 try { 1550 Uri colUri = mContentResolver.insert(uri, testVals); 1551 if (!TextUtils.equals(key, Colors.DATA)) { 1552 // The DATA field is allowed to be empty. 1553 fail("Should not allow color creation without " + key); 1554 } 1555 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1556 } catch (IllegalArgumentException e) { 1557 if (TextUtils.equals(key, Colors.DATA)) { 1558 // The DATA field is allowed to be empty. 1559 fail("Should allow color creation without " + key); 1560 } 1561 } 1562 testVals.put(key, colorValues.getAsString(key)); 1563 } 1564 1565 // Verify writing a color works 1566 Uri col1 = mContentResolver.insert(uri, colorValues); 1567 1568 // Verify adding the same color fails 1569 try { 1570 mContentResolver.insert(uri, colorValues); 1571 fail("Should not allow adding the same color twice"); 1572 } catch (IllegalArgumentException e) { 1573 // WAI 1574 } 1575 1576 // Verify specifying a different account than the query params doesn't work 1577 colorValues.put(Colors.ACCOUNT_NAME, account2); 1578 try { 1579 mContentResolver.insert(uri, colorValues); 1580 fail("Should use the account from the query params, not the values."); 1581 } catch (IllegalArgumentException e) { 1582 // WAI 1583 } 1584 1585 // Verify adding a color to a different account works 1586 Uri col2 = mContentResolver.insert(uri2, colorValues); 1587 1588 // And a different index on the same account 1589 colorValues.put(Colors.COLOR_KEY, "2"); 1590 Uri col3 = mContentResolver.insert(uri2, colorValues); 1591 1592 // Verify that all three colors are in the table 1593 Cursor c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1594 assertEquals(1, c.getCount()); 1595 c.close(); 1596 c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); 1597 assertEquals(2, c.getCount()); 1598 c.close(); 1599 1600 // Verify deleting them works 1601 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1602 ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); 1603 1604 c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1605 assertEquals(0, c.getCount()); 1606 c.close(); 1607 c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); 1608 assertEquals(0, c.getCount()); 1609 c.close(); 1610 } 1611 1612 /** 1613 * Tests Colors interaction with the Calendars table. 1614 */ 1615 @MediumTest 1616 public void testCalendarColors() { 1617 String account = "cc_account"; 1618 int seed = 0; 1619 1620 // Clean up just in case 1621 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1622 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1623 1624 // Test inserting a calendar with an invalid color index 1625 ContentValues cv = CalendarHelper.getNewCalendarValues(account, seed++); 1626 cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex"); 1627 Uri calSyncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE); 1628 Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE); 1629 1630 try { 1631 Uri uri = mContentResolver.insert(calSyncUri, cv); 1632 fail("Should not allow insertion of invalid color index into Calendars"); 1633 } catch (IllegalArgumentException e) { 1634 // WAI 1635 } 1636 1637 // Test updating a calendar with an invalid color index 1638 long calendarId = createAndVerifyCalendar(account, seed++, null); 1639 cv.clear(); 1640 cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex2"); 1641 Uri calendarUri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId); 1642 try { 1643 mContentResolver.update(calendarUri, cv, null, null); 1644 fail("Should not allow update of invalid color index into Calendars"); 1645 } catch (IllegalArgumentException e) { 1646 // WAI 1647 } 1648 1649 assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE)); 1650 1651 // Test that inserting a valid color index works 1652 cv = CalendarHelper.getNewCalendarValues(account, seed++); 1653 cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]); 1654 1655 Uri uri = mContentResolver.insert(calSyncUri, cv); 1656 long calendarId2 = ContentUris.parseId(uri); 1657 assertTrue(calendarId2 >= 0); 1658 // And updates the calendar's color to the one in the table 1659 cv.put(Calendars.CALENDAR_COLOR, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]); 1660 verifyCalendar(account, cv, calendarId2, 2); 1661 1662 // Test that updating a valid color index also updates the color in a 1663 // calendar 1664 cv.clear(); 1665 cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]); 1666 mContentResolver.update(calendarUri, cv, null, null); 1667 Cursor c = mContentResolver.query(calendarUri, 1668 new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR }, 1669 null, null, null); 1670 try { 1671 c.moveToFirst(); 1672 String index = c.getString(0); 1673 int color = c.getInt(1); 1674 assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]); 1675 assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]); 1676 } finally { 1677 if (c != null) { 1678 c.close(); 1679 } 1680 } 1681 1682 // And clearing it doesn't change the color 1683 cv.put(Calendars.CALENDAR_COLOR_KEY, (String) null); 1684 mContentResolver.update(calendarUri, cv, null, null); 1685 c = mContentResolver.query(calendarUri, 1686 new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR }, 1687 null, null, null); 1688 try { 1689 c.moveToFirst(); 1690 String index = c.getString(0); 1691 int color = c.getInt(1); 1692 assertEquals(index, null); 1693 assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0], color); 1694 } finally { 1695 if (c != null) { 1696 c.close(); 1697 } 1698 } 1699 1700 // Test that setting a calendar color to an event color fails 1701 cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0]); 1702 try { 1703 mContentResolver.update(calendarUri, cv, null, null); 1704 fail("Should not allow a calendar to use an event color"); 1705 } catch (IllegalArgumentException e) { 1706 // WAI 1707 } 1708 1709 // Test that you can't remove a color that is referenced by a calendar 1710 cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3]); 1711 mContentResolver.update(calendarUri, cv, null, null); 1712 1713 try { 1714 mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX, 1715 new String[] { 1716 account, CTS_TEST_TYPE, 1717 ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3] 1718 }); 1719 fail("Should not allow deleting referenced color"); 1720 } catch (UnsupportedOperationException e) { 1721 // WAI 1722 } 1723 1724 // Clean up 1725 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1726 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1727 } 1728 1729 /** 1730 * Tests Colors interaction with the Events table. 1731 */ 1732 @MediumTest 1733 public void testEventColors() { 1734 String account = "ec_account"; 1735 int seed = 0; 1736 1737 // Clean up just in case 1738 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1739 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1740 1741 // Test inserting an event with an invalid color index 1742 long cal_id = createAndVerifyCalendar(account, seed++, null); 1743 1744 Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE); 1745 1746 ContentValues ev = EventHelper.getNewEventValues(account, seed++, cal_id, false); 1747 ev.put(Events.EVENT_COLOR_KEY, "badIndex"); 1748 1749 try { 1750 Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev); 1751 fail("Should not allow insertion of invalid color index into Events"); 1752 } catch (IllegalArgumentException e) { 1753 // WAI 1754 } 1755 1756 // Test updating an event with an invalid color index fails 1757 long event_id = createAndVerifyEvent(account, seed++, cal_id, false, null); 1758 ev.clear(); 1759 ev.put(Events.EVENT_COLOR_KEY, "badIndex2"); 1760 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, event_id); 1761 try { 1762 mContentResolver.update(eventUri, ev, null, null); 1763 fail("Should not allow update of invalid color index into Events"); 1764 } catch (IllegalArgumentException e) { 1765 // WAI 1766 } 1767 1768 assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE)); 1769 1770 // Test that inserting a valid color index works 1771 ev = EventHelper.getNewEventValues(account, seed++, cal_id, false); 1772 final String defaultColorIndex = ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0]; 1773 ev.put(Events.EVENT_COLOR_KEY, defaultColorIndex); 1774 1775 Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev); 1776 long eventId2 = ContentUris.parseId(uri); 1777 assertTrue(eventId2 >= 0); 1778 // And updates the event's color to the one in the table 1779 final int expectedColor = ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_0]; 1780 ev.put(Events.EVENT_COLOR, expectedColor); 1781 verifyEvent(ev, eventId2); 1782 1783 // Test that event iterator has COLOR columns 1784 final EntityIterator iterator = EventsEntity.newEntityIterator(mContentResolver.query( 1785 ContentUris.withAppendedId(EventsEntity.CONTENT_URI, eventId2), 1786 null, null, null, null), mContentResolver); 1787 assertTrue("Empty Iterator", iterator.hasNext()); 1788 final Entity entity = iterator.next(); 1789 final ContentValues values = entity.getEntityValues(); 1790 assertTrue("Missing EVENT_COLOR", values.containsKey(EventsEntity.EVENT_COLOR)); 1791 assertEquals("Wrong EVENT_COLOR", 1792 expectedColor, 1793 (int) values.getAsInteger(EventsEntity.EVENT_COLOR)); 1794 assertTrue("Missing EVENT_COLOR_KEY", values.containsKey(EventsEntity.EVENT_COLOR_KEY)); 1795 assertEquals("Wrong EVENT_COLOR_KEY", 1796 defaultColorIndex, 1797 values.getAsString(EventsEntity.EVENT_COLOR_KEY)); 1798 iterator.close(); 1799 1800 // Test that updating a valid color index also updates the color in an 1801 // event 1802 ev.clear(); 1803 ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]); 1804 mContentResolver.update(eventUri, ev, null, null); 1805 Cursor c = mContentResolver.query(eventUri, new String[] { 1806 Events.EVENT_COLOR_KEY, Events.EVENT_COLOR 1807 }, null, null, null); 1808 try { 1809 c.moveToFirst(); 1810 String index = c.getString(0); 1811 int color = c.getInt(1); 1812 assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]); 1813 assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1]); 1814 } finally { 1815 if (c != null) { 1816 c.close(); 1817 } 1818 } 1819 1820 // And clearing it doesn't change the color 1821 ev.put(Events.EVENT_COLOR_KEY, (String) null); 1822 mContentResolver.update(eventUri, ev, null, null); 1823 c = mContentResolver.query(eventUri, new String[] { 1824 Events.EVENT_COLOR_KEY, Events.EVENT_COLOR 1825 }, null, null, null); 1826 try { 1827 c.moveToFirst(); 1828 String index = c.getString(0); 1829 int color = c.getInt(1); 1830 assertEquals(index, null); 1831 assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1], color); 1832 } finally { 1833 if (c != null) { 1834 c.close(); 1835 } 1836 } 1837 1838 // Test that setting an event color to a calendar color fails 1839 ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_2]); 1840 try { 1841 mContentResolver.update(eventUri, ev, null, null); 1842 fail("Should not allow an event to use a calendar color"); 1843 } catch (IllegalArgumentException e) { 1844 // WAI 1845 } 1846 1847 // Test that you can't remove a color that is referenced by an event 1848 ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]); 1849 mContentResolver.update(eventUri, ev, null, null); 1850 try { 1851 mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX, 1852 new String[] { 1853 account, CTS_TEST_TYPE, 1854 ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1] 1855 }); 1856 fail("Should not allow deleting referenced color"); 1857 } catch (UnsupportedOperationException e) { 1858 // WAI 1859 } 1860 1861 // TODO test colors with exceptions 1862 1863 // Clean up 1864 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1865 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); 1866 } 1867 1868 /** 1869 * Tests creation and manipulation of ExtendedProperties. 1870 */ 1871 @MediumTest 1872 public void testExtendedProperties() { 1873 String account = "ep_account"; 1874 int seed = 0; 1875 1876 // Clean up just in case. 1877 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1878 1879 // Create calendar. 1880 long calendarId = createAndVerifyCalendar(account, seed++, null); 1881 1882 // Create events. 1883 ContentValues eventValues; 1884 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 1885 long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 1886 assertTrue(eventId1 >= 0); 1887 1888 /* 1889 * Add some extended properties. 1890 */ 1891 long epId1 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account, 1892 eventId1, "first", "Jeffrey"); 1893 long epId2 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account, 1894 eventId1, "last", "Lebowski"); 1895 long epId3 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account, 1896 eventId1, "title", "Dude"); 1897 1898 /* 1899 * Spot-check a couple of entries. 1900 */ 1901 Cursor cursor = ExtendedPropertiesHelper.findExtendedPropertiesByEventId(mContentResolver, 1902 eventId1); 1903 try { 1904 assertEquals(3, cursor.getCount()); 1905 //DatabaseUtils.dumpCursor(cursor); 1906 1907 while (cursor.moveToNext()) { 1908 String name = 1909 cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_NAME_INDEX); 1910 String value = 1911 cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_VALUE_INDEX); 1912 1913 if (name.equals("last")) { 1914 assertEquals("Lebowski", value); 1915 } 1916 } 1917 1918 String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1, 1919 "title"); 1920 assertEquals("Dude", title); 1921 } finally { 1922 if (cursor != null) { 1923 cursor.close(); 1924 } 1925 } 1926 1927 // Update the title. Must be done as a sync adapter. 1928 ContentValues newValues = new ContentValues(); 1929 newValues.put(ExtendedProperties.VALUE, "Big"); 1930 Uri uri = ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, epId3); 1931 uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); 1932 int count = mContentResolver.update(uri, newValues, null, null); 1933 assertEquals(1, count); 1934 1935 // check it 1936 String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1, 1937 "title"); 1938 assertEquals("Big", title); 1939 1940 removeAndVerifyCalendar(account, calendarId); 1941 } 1942 1943 private class CalendarEventHelper { 1944 1945 private long mCalendarId; 1946 private String mAccount; 1947 private int mSeed; 1948 1949 public CalendarEventHelper(String account, int seed) { 1950 mAccount = account; 1951 mSeed = seed; 1952 ContentValues values = CalendarHelper.getNewCalendarValues(account, seed); 1953 mCalendarId = createAndVerifyCalendar(account, seed++, values); 1954 } 1955 1956 public ContentValues addEvent(String timeString, int timeZoneIndex, long duration) { 1957 long event1Start = timeInMillis(timeString, timeZoneIndex); 1958 ContentValues eventValues; 1959 eventValues = EventHelper.getNewEventValues(mAccount, mSeed++, mCalendarId, true); 1960 eventValues.put(Events.DESCRIPTION, timeString); 1961 eventValues.put(Events.DTSTART, event1Start); 1962 eventValues.put(Events.DTEND, event1Start + duration); 1963 eventValues.put(Events.EVENT_TIMEZONE, TIME_ZONES[timeZoneIndex]); 1964 long eventId = createAndVerifyEvent(mAccount, mSeed, mCalendarId, true, eventValues); 1965 assertTrue(eventId >= 0); 1966 return eventValues; 1967 } 1968 1969 public long getCalendarId() { 1970 return mCalendarId; 1971 } 1972 } 1973 1974 /** 1975 * Test query to retrieve instances within a certain time interval. 1976 */ 1977 public void testWhenByDayQuery() { 1978 String account = "cser_account"; 1979 int seed = 0; 1980 1981 // Clean up just in case 1982 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 1983 1984 // Create a calendar 1985 CalendarEventHelper helper = new CalendarEventHelper(account, seed); 1986 1987 // Add events to the calendar--the first two in the queried range 1988 List<ContentValues> eventsWithinRange = new ArrayList<ContentValues>(); 1989 1990 ContentValues values = helper.addEvent("2009-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS); 1991 eventsWithinRange.add(values); 1992 1993 values = helper.addEvent("2010-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS); 1994 eventsWithinRange.add(values); 1995 1996 helper.addEvent("2011-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS); 1997 1998 // Prepare the start time and end time of the range to query 1999 String startTime = "2009-01-01T00:00:00"; 2000 String endTime = "2011-01-01T00:00:00"; 2001 int julianStart = getJulianDay(startTime, 0); 2002 int julianEnd = getJulianDay(endTime, 0); 2003 Uri uri = Uri.withAppendedPath( 2004 CalendarContract.Instances.CONTENT_BY_DAY_URI, julianStart + "/" + julianEnd); 2005 2006 // Query the range, sorting by event start time 2007 Cursor c = mContentResolver.query(uri, null, Instances.CALENDAR_ID + "=" 2008 + helper.getCalendarId(), null, Events.DTSTART); 2009 2010 // Assert that two events are returned 2011 assertEquals(c.getCount(), 2); 2012 2013 Set<String> keySet = new HashSet(); 2014 keySet.add(Events.DESCRIPTION); 2015 keySet.add(Events.DTSTART); 2016 keySet.add(Events.DTEND); 2017 keySet.add(Events.EVENT_TIMEZONE); 2018 2019 // Verify that the contents of those two events match the cursor results 2020 verifyContentValuesAgainstCursor(eventsWithinRange, keySet, c); 2021 } 2022 2023 private void verifyContentValuesAgainstCursor(List<ContentValues> cvs, 2024 Set<String> keys, Cursor cursor) { 2025 assertEquals(cursor.getCount(), cvs.size()); 2026 2027 cursor.moveToFirst(); 2028 2029 int i=0; 2030 do { 2031 ContentValues cv = cvs.get(i); 2032 for (String key : keys) { 2033 assertEquals(cv.get(key).toString(), 2034 cursor.getString(cursor.getColumnIndex(key))); 2035 } 2036 i++; 2037 } while (cursor.moveToNext()); 2038 2039 cursor.close(); 2040 } 2041 2042 private long timeInMillis(String timeString, int timeZoneIndex) { 2043 Time startTime = new Time(TIME_ZONES[timeZoneIndex]); 2044 startTime.parse3339(timeString); 2045 return startTime.toMillis(false); 2046 } 2047 2048 private int getJulianDay(String timeString, int timeZoneIndex) { 2049 Time time = new Time(TIME_ZONES[timeZoneIndex]); 2050 time.parse3339(timeString); 2051 return Time.getJulianDay(time.toMillis(false), time.gmtoff); 2052 } 2053 2054 /** 2055 * Test instance queries with search parameters. 2056 */ 2057 @MediumTest 2058 public void testInstanceSearch() { 2059 String account = "cser_account"; 2060 int seed = 0; 2061 2062 // Clean up just in case 2063 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2064 2065 // Create a calendar 2066 ContentValues values = CalendarHelper.getNewCalendarValues(account, seed); 2067 long calendarId = createAndVerifyCalendar(account, seed++, values); 2068 2069 String testStart = "2009-10-01T08:00:00"; 2070 String timeZone = TIME_ZONES[0]; 2071 Time startTime = new Time(timeZone); 2072 startTime.parse3339(testStart); 2073 long startMillis = startTime.toMillis(false); 2074 2075 // Create some events, with different descriptions. (Could also create a single 2076 // recurring event and some instance exceptions.) 2077 ContentValues eventValues; 2078 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 2079 eventValues.put(Events.DESCRIPTION, "testevent event-one fiddle"); 2080 eventValues.put(Events.DTSTART, startMillis); 2081 eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS); 2082 eventValues.put(Events.EVENT_TIMEZONE, timeZone); 2083 long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2084 assertTrue(eventId1 >= 0); 2085 2086 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 2087 eventValues.put(Events.DESCRIPTION, "testevent event-two fuzzle"); 2088 eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS); 2089 eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2); 2090 eventValues.put(Events.EVENT_TIMEZONE, timeZone); 2091 long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2092 assertTrue(eventId2 >= 0); 2093 2094 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 2095 eventValues.put(Events.DESCRIPTION, "testevent event-three fiddle"); 2096 eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS * 2); 2097 eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 3); 2098 eventValues.put(Events.EVENT_TIMEZONE, timeZone); 2099 long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2100 assertTrue(eventId3 >= 0); 2101 2102 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 2103 eventValues.put(Events.DESCRIPTION, "nontestevent"); 2104 eventValues.put(Events.DTSTART, startMillis + (long) (DateUtils.HOUR_IN_MILLIS * 1.5f)); 2105 eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2); 2106 eventValues.put(Events.EVENT_TIMEZONE, timeZone); 2107 long eventId4 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2108 assertTrue(eventId4 >= 0); 2109 2110 String rangeStart = "2009-10-01T00:00:00"; 2111 String rangeEnd = "2009-10-01T11:59:59"; 2112 String[] projection = new String[] { Instances.BEGIN }; 2113 2114 if (false) { 2115 Cursor instances = getInstances(timeZone, rangeStart, rangeEnd, projection, 2116 new long[] { calendarId }); 2117 dumpInstances(instances, timeZone, "all"); 2118 instances.close(); 2119 } 2120 2121 Cursor instances; 2122 int count; 2123 2124 // Find all matching "testevent". The search matches on partial strings, so this 2125 // will also pick up "nontestevent". 2126 instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, 2127 "testevent", false, projection, new long[] { calendarId }); 2128 count = instances.getCount(); 2129 instances.close(); 2130 assertEquals(4, count); 2131 2132 // Find all matching "fiddle" and "event". Set the "by day" flag just to be different. 2133 instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, 2134 "fiddle event", true, projection, new long[] { calendarId }); 2135 count = instances.getCount(); 2136 instances.close(); 2137 assertEquals(2, count); 2138 2139 // Find all matching "fiddle" and "baluchitherium". 2140 instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, 2141 "baluchitherium fiddle", false, projection, new long[] { calendarId }); 2142 count = instances.getCount(); 2143 instances.close(); 2144 assertEquals(0, count); 2145 2146 // Find all matching "event-two". 2147 instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, 2148 "event-two", false, projection, new long[] { calendarId }); 2149 count = instances.getCount(); 2150 instances.close(); 2151 assertEquals(1, count); 2152 2153 removeAndVerifyCalendar(account, calendarId); 2154 } 2155 2156 @MediumTest 2157 public void testCalendarUpdateAsApp() { 2158 String account = "cu1_account"; 2159 int seed = 0; 2160 2161 // Clean up just in case 2162 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2163 2164 // Create a calendar 2165 ContentValues values = CalendarHelper.getNewCalendarValues(account, seed); 2166 long id = createAndVerifyCalendar(account, seed++, values); 2167 2168 Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id); 2169 2170 // Update the calendar using the direct Uri 2171 ContentValues updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal( 2172 values, seed++); 2173 assertEquals(1, mContentResolver.update(uri, updateValues, null, null)); 2174 2175 verifyCalendar(account, values, id, 1); 2176 2177 // Update the calendar using selection + args 2178 String selection = Calendars._ID + "=?"; 2179 String[] selectionArgs = new String[] { Long.toString(id) }; 2180 2181 updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal(values, seed++); 2182 2183 assertEquals(1, mContentResolver.update( 2184 Calendars.CONTENT_URI, updateValues, selection, selectionArgs)); 2185 2186 verifyCalendar(account, values, id, 1); 2187 2188 removeAndVerifyCalendar(account, id); 2189 } 2190 2191 // TODO test calendar updates as sync adapter 2192 2193 /** 2194 * Test access to the "syncstate" table. 2195 */ 2196 @MediumTest 2197 public void testSyncState() { 2198 String account = "ss_account"; 2199 int seed = 0; 2200 2201 // Clean up just in case 2202 SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true); 2203 2204 // Create a new sync state entry 2205 ContentValues values = SyncStateHelper.getNewSyncStateValues(account); 2206 long id = createAndVerifySyncState(account, values); 2207 2208 // Look it up with the by-ID URI 2209 Cursor c = SyncStateHelper.getSyncStateById(mContentResolver, id); 2210 assertNotNull(c); 2211 assertEquals(1, c.getCount()); 2212 c.close(); 2213 2214 // Try to remove it as non-sync-adapter; expected to fail. 2215 boolean failed; 2216 try { 2217 SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, false); 2218 failed = false; 2219 } catch (IllegalArgumentException iae) { 2220 failed = true; 2221 } 2222 assertTrue("deletion of sync state by app was allowed", failed); 2223 2224 // Remove it and verify that it's gone 2225 removeAndVerifySyncState(account); 2226 } 2227 2228 2229 private void verifyEvent(ContentValues values, long eventId) { 2230 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); 2231 // Verify 2232 Cursor c = mContentResolver 2233 .query(eventUri, EventHelper.EVENTS_PROJECTION, null, null, null); 2234 assertEquals(1, c.getCount()); 2235 assertTrue(c.moveToFirst()); 2236 assertEquals(eventId, c.getLong(0)); 2237 for (String key : values.keySet()) { 2238 int index = c.getColumnIndex(key); 2239 assertEquals(key, values.getAsString(key), c.getString(index)); 2240 } 2241 c.close(); 2242 } 2243 2244 @MediumTest 2245 public void testEventCreationAndDeletion() { 2246 String account = "ec1_account"; 2247 int seed = 0; 2248 2249 // Clean up just in case 2250 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2251 2252 // Create calendar and event 2253 long calendarId = createAndVerifyCalendar(account, seed++, null); 2254 2255 ContentValues eventValues = EventHelper 2256 .getNewEventValues(account, seed++, calendarId, true); 2257 long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2258 2259 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); 2260 2261 removeAndVerifyEvent(eventUri, eventValues, account); 2262 2263 // Attempt to create an event without a calendar ID. 2264 ContentValues badValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 2265 badValues.remove(Events.CALENDAR_ID); 2266 try { 2267 createAndVerifyEvent(account, seed, calendarId, true, badValues); 2268 fail("was allowed to create an event without CALENDAR_ID"); 2269 } catch (IllegalArgumentException iae) { 2270 // expected 2271 } 2272 2273 // Validation may be relaxed for content providers, so test missing timezone as app. 2274 badValues = EventHelper.getNewEventValues(account, seed++, calendarId, false); 2275 badValues.remove(Events.EVENT_TIMEZONE); 2276 try { 2277 createAndVerifyEvent(account, seed, calendarId, false, badValues); 2278 fail("was allowed to create an event without EVENT_TIMEZONE"); 2279 } catch (IllegalArgumentException iae) { 2280 // expected 2281 } 2282 2283 removeAndVerifyCalendar(account, calendarId); 2284 } 2285 2286 @MediumTest 2287 public void testEventUpdateAsApp() { 2288 String account = "em1_account"; 2289 int seed = 0; 2290 2291 // Clean up just in case 2292 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2293 2294 // Create calendar 2295 long calendarId = createAndVerifyCalendar(account, seed++, null); 2296 2297 // Create event as sync adapter 2298 ContentValues eventValues = EventHelper 2299 .getNewEventValues(account, seed++, calendarId, true); 2300 long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2301 2302 // Update event as app 2303 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); 2304 2305 ContentValues updateValues = EventHelper.getUpdateEventValuesWithOriginal(eventValues, 2306 seed++, false); 2307 assertEquals(1, mContentResolver.update(eventUri, updateValues, null, null)); 2308 updateValues.put(Events.DIRTY, 1); // provider should have marked as dirty 2309 verifyEvent(updateValues, eventId); 2310 2311 // Try nulling out a required value. 2312 ContentValues badValues = new ContentValues(updateValues); 2313 badValues.putNull(Events.EVENT_TIMEZONE); 2314 badValues.remove(Events.DIRTY); 2315 try { 2316 mContentResolver.update(eventUri, badValues, null, null); 2317 fail("was allowed to null out EVENT_TIMEZONE"); 2318 } catch (IllegalArgumentException iae) { 2319 // good 2320 } 2321 2322 removeAndVerifyEvent(eventUri, eventValues, account); 2323 2324 // delete the calendar 2325 removeAndVerifyCalendar(account, calendarId); 2326 } 2327 2328 /** 2329 * Tests update of multiple events with a single update call. 2330 */ 2331 @MediumTest 2332 public void testBulkUpdate() { 2333 String account = "bup_account"; 2334 int seed = 0; 2335 2336 // Clean up just in case 2337 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2338 2339 // Create calendar 2340 long calendarId = createAndVerifyCalendar(account, seed++, null); 2341 String calendarIdStr = String.valueOf(calendarId); 2342 2343 // Create events 2344 ContentValues eventValues; 2345 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 2346 long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2347 2348 eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); 2349 long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2350 2351 // Update the "description" field in all events in this calendar. 2352 String newDescription = "bulk edit"; 2353 ContentValues updateValues = new ContentValues(); 2354 updateValues.put(Events.DESCRIPTION, newDescription); 2355 2356 // Must be sync adapter to do a bulk update. 2357 Uri uri = asSyncAdapter(Events.CONTENT_URI, account, CTS_TEST_TYPE); 2358 int count = mContentResolver.update(uri, updateValues, SQL_WHERE_CALENDAR_ID, 2359 new String[] { calendarIdStr }); 2360 2361 // Check to see if the changes went through. 2362 Uri eventUri = Events.CONTENT_URI; 2363 Cursor c = mContentResolver.query(eventUri, new String[] { Events.DESCRIPTION }, 2364 SQL_WHERE_CALENDAR_ID, new String[] { calendarIdStr }, null); 2365 assertEquals(2, c.getCount()); 2366 while (c.moveToNext()) { 2367 assertEquals(newDescription, c.getString(0)); 2368 } 2369 c.close(); 2370 2371 // delete the calendar 2372 removeAndVerifyCalendar(account, calendarId); 2373 } 2374 2375 /** 2376 * Tests the content provider's enforcement of restrictions on who is allowed to modify 2377 * specific columns in a Calendar. 2378 * <p> 2379 * This attempts to create a new row in the Calendar table, specifying one restricted 2380 * column at a time. 2381 */ 2382 @MediumTest 2383 public void testSyncOnlyInsertEnforcement() { 2384 // These operations should not succeed, so there should be nothing to clean up after. 2385 // TODO: this should be a new event augmented with an illegal column, not a single 2386 // column. Otherwise we might be tripping over a "DTSTART must exist" test. 2387 ContentValues vals = new ContentValues(); 2388 for (int i = 0; i < Calendars.SYNC_WRITABLE_COLUMNS.length; i++) { 2389 boolean threw = false; 2390 try { 2391 vals.clear(); 2392 vals.put(Calendars.SYNC_WRITABLE_COLUMNS[i], "1"); 2393 mContentResolver.insert(Calendars.CONTENT_URI, vals); 2394 } catch (IllegalArgumentException e) { 2395 threw = true; 2396 } 2397 assertTrue("Only sync adapter should be allowed to insert " 2398 + Calendars.SYNC_WRITABLE_COLUMNS[i], threw); 2399 } 2400 } 2401 2402 /** 2403 * Tests creation of a recurring event. 2404 * <p> 2405 * This (and the other recurrence tests) uses dates well in the past to reduce the likelihood 2406 * of encountering non-test recurring events. (Ideally we would select events associated 2407 * with a specific calendar.) With dates well in the past, it's also important to have a 2408 * fixed maximum count or end date; otherwise, if the metadata min/max instance values are 2409 * large enough, the recurrence recalculation processor could get triggered on an insert or 2410 * update and bump up against the 2000-instance limit. 2411 * 2412 * TODO: need some allDay tests 2413 */ 2414 @MediumTest 2415 public void testRecurrence() { 2416 String account = "re_account"; 2417 int seed = 0; 2418 2419 // Clean up just in case 2420 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2421 2422 // Create calendar 2423 long calendarId = createAndVerifyCalendar(account, seed++, null); 2424 2425 // Create recurring event 2426 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, 2427 calendarId, true, "2003-08-05T09:00:00", "PT1H", 2428 "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU"); 2429 long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2430 //Log.d(TAG, "+++ basic recurrence eventId is " + eventId); 2431 2432 // Check to see if we have the expected number of instances 2433 String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); 2434 int instanceCount = getInstanceCount(timeZone, "2003-08-05T00:00:00", 2435 "2003-08-31T11:59:59", new long[] { calendarId }); 2436 if (false) { 2437 Cursor instances = getInstances(timeZone, "2003-08-05T00:00:00", "2003-08-31T11:59:59", 2438 new String[] { Instances.BEGIN }, new long[] { calendarId }); 2439 dumpInstances(instances, timeZone, "initial"); 2440 instances.close(); 2441 } 2442 assertEquals("recurrence instance count", 4, instanceCount); 2443 2444 // delete the calendar 2445 removeAndVerifyCalendar(account, calendarId); 2446 } 2447 2448 /** 2449 * Tests conversion of a regular event to a recurring event. 2450 */ 2451 @MediumTest 2452 public void testConversionToRecurring() { 2453 String account = "reconv_account"; 2454 int seed = 0; 2455 2456 // Clean up just in case 2457 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2458 2459 // Create calendar and event 2460 long calendarId = createAndVerifyCalendar(account, seed++, null); 2461 2462 ContentValues eventValues = EventHelper 2463 .getNewEventValues(account, seed++, calendarId, true); 2464 long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); 2465 2466 long dtstart = eventValues.getAsLong(Events.DTSTART); 2467 long dtend = eventValues.getAsLong(Events.DTEND); 2468 long durationSecs = (dtend - dtstart) / 1000; 2469 2470 ContentValues updateValues = new ContentValues(); 2471 updateValues.put(Events.RRULE, "FREQ=WEEKLY"); // recurs forever 2472 updateValues.put(Events.DURATION, "P" + durationSecs + "S"); 2473 updateValues.putNull(Events.DTEND); 2474 2475 // Issue update; do it as app instead of sync adapter to exercise that path. 2476 updateAndVerifyEvent(account, calendarId, eventId, false, updateValues); 2477 2478 // Make sure LAST_DATE got nulled out by our infinitely repeating sequence. 2479 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); 2480 Cursor c = mContentResolver.query(eventUri, new String[] { Events.LAST_DATE }, 2481 null, null, null); 2482 assertEquals(1, c.getCount()); 2483 assertTrue(c.moveToFirst()); 2484 assertNull(c.getString(0)); 2485 c.close(); 2486 2487 removeAndVerifyCalendar(account, calendarId); 2488 } 2489 2490 /** 2491 * Tests creation of a recurring event with single-instance exceptions. 2492 */ 2493 @MediumTest 2494 public void testSingleRecurrenceExceptions() { 2495 String account = "rex_account"; 2496 int seed = 0; 2497 2498 // Clean up just in case 2499 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2500 2501 // Create calendar 2502 long calendarId = createAndVerifyCalendar(account, seed++, null); 2503 2504 // Create recurring event. 2505 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, 2506 calendarId, true, "1999-03-28T09:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=100"); 2507 long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); 2508 2509 // Add some attendees and reminders. 2510 addAttendees(account, eventId, seed); 2511 addReminders(account, eventId, seed); 2512 2513 // Select a period that gives us 5 instances. We don't want this to straddle a DST 2514 // transition, because we expect the startMinute field to be the same for all 2515 // instances, and it's stored as minutes since midnight in the device's time zone. 2516 // Things won't be consistent if the event and the device have different ideas about DST. 2517 String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); 2518 String testStart = "1999-04-18T00:00:00"; 2519 String testEnd = "1999-05-16T23:59:59"; 2520 String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.END_MINUTE }; 2521 2522 Cursor instances = getInstances(timeZone, testStart, testEnd, projection, 2523 new long[] { calendarId }); 2524 if (DEBUG_RECURRENCE) { 2525 dumpInstances(instances, timeZone, "initial"); 2526 } 2527 2528 assertEquals("initial recurrence instance count", 5, instances.getCount()); 2529 2530 /* 2531 * Advance the start time of a few instances, and verify. 2532 */ 2533 2534 // Leave first instance alone. 2535 instances.moveToPosition(1); 2536 2537 long startMillis; 2538 ContentValues excepValues; 2539 2540 // Advance the start time of the 2nd instance. 2541 startMillis = instances.getLong(0); 2542 excepValues = EventHelper.getNewExceptionValues(startMillis); 2543 excepValues.put(Events.DTSTART, startMillis + 3600*1000); 2544 long excepEventId2 = createAndVerifyException(account, eventId, excepValues, true); 2545 instances.moveToNext(); 2546 2547 // Advance the start time of the 3rd instance. 2548 startMillis = instances.getLong(0); 2549 excepValues = EventHelper.getNewExceptionValues(startMillis); 2550 excepValues.put(Events.DTSTART, startMillis + 3600*1000*2); 2551 long excepEventId3 = createAndVerifyException(account, eventId, excepValues, true); 2552 instances.moveToNext(); 2553 2554 // Cancel the 4th instance. 2555 startMillis = instances.getLong(0); 2556 excepValues = EventHelper.getNewExceptionValues(startMillis); 2557 excepValues.put(Events.STATUS, Events.STATUS_CANCELED); 2558 long excepEventId4 = createAndVerifyException(account, eventId, excepValues, true); 2559 instances.moveToNext(); 2560 2561 // TODO: try to modify a non-existent instance. 2562 2563 instances.close(); 2564 2565 // TODO: compare Reminders, Attendees, ExtendedProperties on one of the exception events 2566 2567 // Re-query the instances and figure out if they look right. 2568 instances = getInstances(timeZone, testStart, testEnd, projection, 2569 new long[] { calendarId }); 2570 if (DEBUG_RECURRENCE) { 2571 dumpInstances(instances, timeZone, "with DTSTART exceptions"); 2572 } 2573 assertEquals("exceptional recurrence instance count", 4, instances.getCount()); 2574 2575 long prevMinute = -1; 2576 while (instances.moveToNext()) { 2577 // expect the start times for each entry to be different from the previous entry 2578 long startMinute = instances.getLong(1); 2579 assertTrue("instance start times are different", startMinute != prevMinute); 2580 2581 prevMinute = startMinute; 2582 } 2583 instances.close(); 2584 2585 2586 // Delete all of our exceptions, and verify. 2587 int deleteCount = 0; 2588 deleteCount += deleteException(account, eventId, excepEventId2); 2589 deleteCount += deleteException(account, eventId, excepEventId3); 2590 deleteCount += deleteException(account, eventId, excepEventId4); 2591 assertEquals("events deleted", 3, deleteCount); 2592 2593 // Re-query the instances and figure out if they look right. 2594 instances = getInstances(timeZone, testStart, testEnd, projection, 2595 new long[] { calendarId }); 2596 if (DEBUG_RECURRENCE) { 2597 dumpInstances(instances, timeZone, "post exception deletion"); 2598 } 2599 assertEquals("post-exception deletion instance count", 5, instances.getCount()); 2600 2601 prevMinute = -1; 2602 while (instances.moveToNext()) { 2603 // expect the start times for each entry to be the same 2604 long startMinute = instances.getLong(1); 2605 if (prevMinute != -1) { 2606 assertEquals("instance start times are the same", startMinute, prevMinute); 2607 } 2608 prevMinute = startMinute; 2609 } 2610 instances.close(); 2611 2612 /* 2613 * Repeat the test, this time modifying DURATION. 2614 */ 2615 2616 instances = getInstances(timeZone, testStart, testEnd, projection, 2617 new long[] { calendarId }); 2618 if (DEBUG_RECURRENCE) { 2619 dumpInstances(instances, timeZone, "initial"); 2620 } 2621 2622 assertEquals("initial recurrence instance count", 5, instances.getCount()); 2623 2624 // Leave first instance alone. 2625 instances.moveToPosition(1); 2626 2627 // Advance the end time of the 2nd instance. 2628 startMillis = instances.getLong(0); 2629 excepValues = EventHelper.getNewExceptionValues(startMillis); 2630 excepValues.put(Events.DURATION, "P" + 3600*2 + "S"); 2631 excepEventId2 = createAndVerifyException(account, eventId, excepValues, true); 2632 instances.moveToNext(); 2633 2634 // Advance the end time of the 3rd instance, and change the self-attendee status. 2635 startMillis = instances.getLong(0); 2636 excepValues = EventHelper.getNewExceptionValues(startMillis); 2637 excepValues.put(Events.DURATION, "P" + 3600*3 + "S"); 2638 excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED); 2639 excepEventId3 = createAndVerifyException(account, eventId, excepValues, true); 2640 instances.moveToNext(); 2641 2642 // Advance the start time of the 4th instance, which will also advance the end time. 2643 startMillis = instances.getLong(0); 2644 excepValues = EventHelper.getNewExceptionValues(startMillis); 2645 excepValues.put(Events.DTSTART, startMillis + 3600*1000); 2646 excepEventId4 = createAndVerifyException(account, eventId, excepValues, true); 2647 instances.moveToNext(); 2648 2649 instances.close(); 2650 2651 // TODO: make sure the selfAttendeeStatus change took 2652 2653 // Re-query the instances and figure out if they look right. 2654 instances = getInstances(timeZone, testStart, testEnd, projection, 2655 new long[] { calendarId }); 2656 if (DEBUG_RECURRENCE) { 2657 dumpInstances(instances, timeZone, "with DURATION exceptions"); 2658 } 2659 assertEquals("exceptional recurrence instance count", 5, instances.getCount()); 2660 2661 prevMinute = -1; 2662 while (instances.moveToNext()) { 2663 // expect the start times for each entry to be different from the previous entry 2664 long endMinute = instances.getLong(2); 2665 assertTrue("instance end times are different", endMinute != prevMinute); 2666 2667 prevMinute = endMinute; 2668 } 2669 instances.close(); 2670 2671 // delete the calendar 2672 removeAndVerifyCalendar(account, calendarId); 2673 } 2674 2675 /** 2676 * Tests creation of a simple recurrence exception when not pretending to be the sync 2677 * adapter. One significant consequence is that we don't set the _sync_id field in the 2678 * events, which affects how the provider correlates recurrences and exceptions. 2679 */ 2680 @MediumTest 2681 public void testNonAdapterRecurrenceExceptions() { 2682 String account = "rena_account"; 2683 int seed = 0; 2684 2685 // Clean up just in case 2686 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2687 2688 // Create calendar 2689 long calendarId = createAndVerifyCalendar(account, seed++, null); 2690 2691 // Generate recurring event, with "asSyncAdapter" set to false. 2692 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, 2693 calendarId, false, "1991-02-03T12:00:00", "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10"); 2694 2695 // Select a period that gives us 3 instances. 2696 String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); 2697 String testStart = "1991-02-03T00:00:00"; 2698 String testEnd = "1991-02-05T23:59:59"; 2699 String[] projection = { Instances.BEGIN, Instances.START_MINUTE }; 2700 2701 // Expand the bounds of the instances table so we expand future events as they are added. 2702 expandInstanceRange(account, calendarId, testStart, testEnd, timeZone); 2703 2704 // Create the event in the database. 2705 long eventId = createAndVerifyEvent(account, seed++, calendarId, false, eventValues); 2706 assertTrue(eventId >= 0); 2707 2708 // Add some attendees. 2709 addAttendees(account, eventId, seed); 2710 2711 Cursor instances = getInstances(timeZone, testStart, testEnd, projection, 2712 new long[] { calendarId }); 2713 if (DEBUG_RECURRENCE) { 2714 dumpInstances(instances, timeZone, "initial"); 2715 } 2716 assertEquals("initial recurrence instance count", 3, instances.getCount()); 2717 2718 /* 2719 * Alter the attendee status of the second event. This should cause the instances to 2720 * be updated, replacing the previous 2nd instance with the exception instance. If the 2721 * code is broken we'll see four instances (because the original instance didn't get 2722 * removed) or one instance (because the code correctly deleted all related events but 2723 * couldn't correlate the exception with its original recurrence). 2724 */ 2725 2726 // Leave first instance alone. 2727 instances.moveToPosition(1); 2728 2729 long startMillis; 2730 ContentValues excepValues; 2731 2732 // Advance the start time of the 2nd instance. 2733 startMillis = instances.getLong(0); 2734 excepValues = EventHelper.getNewExceptionValues(startMillis); 2735 excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED); 2736 long excepEventId2 = createAndVerifyException(account, eventId, excepValues, false); 2737 instances.moveToNext(); 2738 2739 instances.close(); 2740 2741 // Re-query the instances and figure out if they look right. 2742 instances = getInstances(timeZone, testStart, testEnd, projection, 2743 new long[] { calendarId }); 2744 if (DEBUG_RECURRENCE) { 2745 dumpInstances(instances, timeZone, "with exceptions"); 2746 } 2747 2748 // TODO: this test currently fails due to limitations in the provider 2749 //assertEquals("exceptional recurrence instance count", 3, instances.getCount()); 2750 2751 instances.close(); 2752 2753 // delete the calendar 2754 removeAndVerifyCalendar(account, calendarId); 2755 } 2756 2757 /** 2758 * Tests insertion of event exceptions before and after a recurring event is created. 2759 * <p> 2760 * The server may send exceptions down before the event they refer to, so the provider 2761 * fills in the originalId of previously-existing exceptions when a recurring event is 2762 * inserted. Make sure that works. 2763 * <p> 2764 * The _sync_id column is only unique with a given calendar. We create events with 2765 * identical originalSyncId values in two different calendars to verify that the provider 2766 * doesn't update unrelated events. 2767 * <p> 2768 * We can't use the /exception URI, because that only works if the events are created 2769 * in order. 2770 */ 2771 @MediumTest 2772 public void testOutOfOrderRecurrenceExceptions() { 2773 String account1 = "roid1_account"; 2774 String account2 = "roid2_account"; 2775 String startWhen = "1987-08-09T12:00:00"; 2776 int seed = 0; 2777 2778 // Clean up just in case 2779 CalendarHelper.deleteCalendarByAccount(mContentResolver, account1); 2780 CalendarHelper.deleteCalendarByAccount(mContentResolver, account2); 2781 2782 // Create calendars 2783 long calendarId1 = createAndVerifyCalendar(account1, seed++, null); 2784 long calendarId2 = createAndVerifyCalendar(account2, seed++, null); 2785 2786 2787 // Generate base event. 2788 ContentValues recurEventValues = EventHelper.getNewRecurringEventValues(account1, seed++, 2789 calendarId1, true, startWhen, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10"); 2790 2791 // Select a period that gives us 3 instances. 2792 String timeZone = recurEventValues.getAsString(Events.EVENT_TIMEZONE); 2793 String testStart = "1987-08-09T00:00:00"; 2794 String testEnd = "1987-08-11T23:59:59"; 2795 String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.EVENT_ID }; 2796 2797 /* 2798 * We're interested in exploring what the instance expansion code does with the events 2799 * as they arrive. It won't do anything at event-creation time unless the instance 2800 * range already covers the interesting set of dates, so we need to create and remove 2801 * an instance in the same time frame beforehand. 2802 */ 2803 expandInstanceRange(account1, calendarId1, testStart, testEnd, timeZone); 2804 2805 /* 2806 * Instances table should be expanded. Do the test. 2807 */ 2808 2809 final String MAGIC_SYNC_ID = "MagicSyncId"; 2810 recurEventValues.put(Events._SYNC_ID, MAGIC_SYNC_ID); 2811 2812 // Generate exceptions from base, removing the generated _sync_id and setting the 2813 // base event's _sync_id as originalSyncId. 2814 ContentValues beforeExcepValues, afterExcepValues, unrelatedExcepValues; 2815 beforeExcepValues = new ContentValues(recurEventValues); 2816 afterExcepValues = new ContentValues(recurEventValues); 2817 unrelatedExcepValues = new ContentValues(recurEventValues); 2818 beforeExcepValues.remove(Events._SYNC_ID); 2819 afterExcepValues.remove(Events._SYNC_ID); 2820 unrelatedExcepValues.remove(Events._SYNC_ID); 2821 beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); 2822 afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); 2823 unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); 2824 2825 // Disassociate the "unrelated" exception by moving it to the other calendar. 2826 unrelatedExcepValues.put(Events.CALENDAR_ID, calendarId2); 2827 2828 // We shift the start time by half an hour, and use the same _sync_id. 2829 final long ONE_DAY_MILLIS = 24 * 60 * 60 * 1000; 2830 final long ONE_HOUR_MILLIS = 60 * 60 * 1000; 2831 final long HALF_HOUR_MILLIS = 30 * 60 * 1000; 2832 long dtstartMillis = recurEventValues.getAsLong(Events.DTSTART) + ONE_DAY_MILLIS; 2833 beforeExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis); 2834 beforeExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS); 2835 beforeExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS); 2836 beforeExcepValues.remove(Events.DURATION); 2837 beforeExcepValues.remove(Events.RRULE); 2838 beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); 2839 dtstartMillis += ONE_DAY_MILLIS; 2840 afterExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis); 2841 afterExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS); 2842 afterExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS); 2843 afterExcepValues.remove(Events.DURATION); 2844 afterExcepValues.remove(Events.RRULE); 2845 afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); 2846 dtstartMillis += ONE_DAY_MILLIS; 2847 unrelatedExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis); 2848 unrelatedExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS); 2849 unrelatedExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS); 2850 unrelatedExcepValues.remove(Events.DURATION); 2851 unrelatedExcepValues.remove(Events.RRULE); 2852 unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); 2853 2854 2855 // Create "before" and "unrelated" exceptions. 2856 long beforeEventId = createAndVerifyEvent(account1, seed, calendarId1, true, 2857 beforeExcepValues); 2858 assertTrue(beforeEventId >= 0); 2859 long unrelatedEventId = createAndVerifyEvent(account2, seed, calendarId2, true, 2860 unrelatedExcepValues); 2861 assertTrue(unrelatedEventId >= 0); 2862 2863 // Create recurring event. 2864 long recurEventId = createAndVerifyEvent(account1, seed, calendarId1, true, 2865 recurEventValues); 2866 assertTrue(recurEventId >= 0); 2867 2868 // Create "after" exception. 2869 long afterEventId = createAndVerifyEvent(account1, seed, calendarId1, true, 2870 afterExcepValues); 2871 assertTrue(afterEventId >= 0); 2872 2873 if (Log.isLoggable(TAG, Log.DEBUG)) { 2874 Log.d(TAG, "before=" + beforeEventId + ", unrel=" + unrelatedEventId + 2875 ", recur=" + recurEventId + ", after=" + afterEventId); 2876 } 2877 2878 // Check to see how many instances we get. If the recurrence and the exception don't 2879 // get paired up correctly, we'll see too many instances. 2880 Cursor instances = getInstances(timeZone, testStart, testEnd, projection, 2881 new long[] { calendarId1, calendarId2 }); 2882 if (DEBUG_RECURRENCE) { 2883 dumpInstances(instances, timeZone, "with exception"); 2884 } 2885 2886 assertEquals("initial recurrence instance count", 3, instances.getCount()); 2887 2888 instances.close(); 2889 2890 2891 /* 2892 * Now we want to verify that: 2893 * - "before" and "after" have an originalId equal to our recurEventId 2894 * - "unrelated" has no originalId 2895 */ 2896 Cursor c = null; 2897 try { 2898 final String[] PROJECTION = new String[] { Events.ORIGINAL_ID }; 2899 Uri eventUri; 2900 Long originalId; 2901 2902 eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, beforeEventId); 2903 c = mContentResolver.query(eventUri, PROJECTION, null, null, null); 2904 assertEquals(1, c.getCount()); 2905 c.moveToNext(); 2906 originalId = c.getLong(0); 2907 assertNotNull(originalId); 2908 assertEquals(recurEventId, (long) originalId); 2909 c.close(); 2910 2911 eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, afterEventId); 2912 c = mContentResolver.query(eventUri, PROJECTION, null, null, null); 2913 assertEquals(1, c.getCount()); 2914 c.moveToNext(); 2915 originalId = c.getLong(0); 2916 assertNotNull(originalId); 2917 assertEquals(recurEventId, (long) originalId); 2918 c.close(); 2919 2920 eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, unrelatedEventId); 2921 c = mContentResolver.query(eventUri, PROJECTION, null, null, null); 2922 assertEquals(1, c.getCount()); 2923 c.moveToNext(); 2924 assertNull(c.getString(0)); 2925 c.close(); 2926 2927 c = null; 2928 } finally { 2929 if (c != null) { 2930 c.close(); 2931 } 2932 } 2933 2934 // delete the calendars 2935 removeAndVerifyCalendar(account1, calendarId1); 2936 removeAndVerifyCalendar(account2, calendarId2); 2937 } 2938 2939 /** 2940 * Tests exceptions that modify all future instances of a recurring event. 2941 */ 2942 @MediumTest 2943 public void testForwardRecurrenceExceptions() { 2944 String account = "refx_account"; 2945 int seed = 0; 2946 2947 // Clean up just in case 2948 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 2949 2950 // Create calendar 2951 long calendarId = createAndVerifyCalendar(account, seed++, null); 2952 2953 // Create recurring event 2954 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, 2955 calendarId, true, "1999-01-01T06:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=10"); 2956 long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); 2957 2958 // Add some attendees and reminders. 2959 addAttendees(account, eventId, seed++); 2960 addReminders(account, eventId, seed++); 2961 2962 // Get some instances. 2963 String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); 2964 String testStart = "1999-01-01T00:00:00"; 2965 String testEnd = "1999-01-29T23:59:59"; 2966 String[] projection = { Instances.BEGIN, Instances.START_MINUTE }; 2967 2968 Cursor instances = getInstances(timeZone, testStart, testEnd, projection, 2969 new long[] { calendarId }); 2970 if (DEBUG_RECURRENCE) { 2971 dumpInstances(instances, timeZone, "initial"); 2972 } 2973 2974 assertEquals("initial recurrence instance count", 5, instances.getCount()); 2975 2976 // Modify starting from 3rd instance. 2977 instances.moveToPosition(2); 2978 2979 long startMillis; 2980 ContentValues excepValues; 2981 2982 // Replace with a new recurrence rule. We move the start time an hour later, and cap 2983 // it at two instances. 2984 startMillis = instances.getLong(0); 2985 excepValues = EventHelper.getNewExceptionValues(startMillis); 2986 excepValues.put(Events.DTSTART, startMillis + 3600*1000); 2987 excepValues.put(Events.RRULE, "FREQ=WEEKLY;COUNT=2;WKST=SU"); 2988 long excepEventId = createAndVerifyException(account, eventId, excepValues, true); 2989 instances.close(); 2990 2991 2992 // Check to see if it took. 2993 instances = getInstances(timeZone, testStart, testEnd, projection, 2994 new long[] { calendarId }); 2995 if (DEBUG_RECURRENCE) { 2996 dumpInstances(instances, timeZone, "with new rule"); 2997 } 2998 2999 assertEquals("count with exception", 4, instances.getCount()); 3000 3001 long prevMinute = -1; 3002 for (int i = 0; i < 4; i++) { 3003 long startMinute; 3004 instances.moveToNext(); 3005 switch (i) { 3006 case 0: 3007 startMinute = instances.getLong(1); 3008 break; 3009 case 1: 3010 case 3: 3011 startMinute = instances.getLong(1); 3012 assertEquals("first/last pairs match", prevMinute, startMinute); 3013 break; 3014 case 2: 3015 startMinute = instances.getLong(1); 3016 assertFalse("first two != last two", prevMinute == startMinute); 3017 break; 3018 default: 3019 fail(); 3020 startMinute = -1; // make compiler happy 3021 break; 3022 } 3023 3024 prevMinute = startMinute; 3025 } 3026 instances.close(); 3027 3028 // delete the calendar 3029 removeAndVerifyCalendar(account, calendarId); 3030 } 3031 3032 /** 3033 * Tests exceptions that modify all instances of a recurring event. This is not really an 3034 * exception, since it won't create a new event, but supporting it allows us to use the 3035 * exception URI without having to determine whether the "start from here" instance is the 3036 * very first instance. 3037 */ 3038 @MediumTest 3039 public void testFullRecurrenceUpdate() { 3040 String account = "ref_account"; 3041 int seed = 0; 3042 3043 // Clean up just in case 3044 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3045 3046 // Create calendar 3047 long calendarId = createAndVerifyCalendar(account, seed++, null); 3048 3049 // Create recurring event 3050 String rrule = "FREQ=DAILY;WKST=MO;COUNT=100"; 3051 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, 3052 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule); 3053 long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); 3054 //Log.i(TAG, "+++ eventId is " + eventId); 3055 3056 // Get some instances. 3057 String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); 3058 String testStart = "1997-08-01T00:00:00"; 3059 String testEnd = "1997-08-31T23:59:59"; 3060 String[] projection = { Instances.BEGIN, Instances.EVENT_LOCATION }; 3061 String newLocation = "NEW!"; 3062 3063 Cursor instances = getInstances(timeZone, testStart, testEnd, projection, 3064 new long[] { calendarId }); 3065 if (DEBUG_RECURRENCE) { 3066 dumpInstances(instances, timeZone, "initial"); 3067 } 3068 3069 assertEquals("initial recurrence instance count", 3, instances.getCount()); 3070 3071 instances.moveToFirst(); 3072 long startMillis = instances.getLong(0); 3073 ContentValues excepValues = EventHelper.getNewExceptionValues(startMillis); 3074 excepValues.put(Events.RRULE, rrule); // identifies this as an "all future events" excep 3075 excepValues.put(Events.EVENT_LOCATION, newLocation); 3076 long excepEventId = createAndVerifyException(account, eventId, excepValues, true); 3077 instances.close(); 3078 3079 // Check results. 3080 assertEquals("full update does not create new ID", eventId, excepEventId); 3081 3082 instances = getInstances(timeZone, testStart, testEnd, projection, 3083 new long[] { calendarId }); 3084 assertEquals("post-update instance count", 3, instances.getCount()); 3085 while (instances.moveToNext()) { 3086 assertEquals("new location", newLocation, instances.getString(1)); 3087 } 3088 instances.close(); 3089 3090 // delete the calendar 3091 removeAndVerifyCalendar(account, calendarId); 3092 } 3093 3094 @MediumTest 3095 public void testMultiRuleRecurrence() { 3096 String account = "multirule_account"; 3097 int seed = 0; 3098 3099 // Clean up just in case 3100 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3101 3102 // Create calendar 3103 long calendarId = createAndVerifyCalendar(account, seed++, null); 3104 3105 // Create recurring event 3106 String rrule = "FREQ=DAILY;WKST=MO;COUNT=5\nFREQ=WEEKLY;WKST=SU;COUNT=5"; 3107 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, 3108 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule); 3109 long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); 3110 3111 // TODO: once multi-rule RRULEs are fully supported, verify that they work 3112 3113 // delete the calendar 3114 removeAndVerifyCalendar(account, calendarId); 3115 } 3116 3117 /** 3118 * Issue bad requests and expect them to get rejected. 3119 */ 3120 @MediumTest 3121 public void testBadRequests() { 3122 String account = "neg_account"; 3123 int seed = 0; 3124 3125 // Clean up just in case 3126 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3127 3128 // Create calendar 3129 long calendarId = createAndVerifyCalendar(account, seed++, null); 3130 3131 // Create recurring event 3132 String rrule = "FREQ=OFTEN;WKST=MO"; 3133 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, 3134 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule); 3135 try { 3136 createAndVerifyEvent(account, seed++, calendarId, true, eventValues); 3137 fail("Bad recurrence rule should have been rejected"); 3138 } catch (IllegalArgumentException iae) { 3139 // good 3140 } 3141 3142 // delete the calendar 3143 removeAndVerifyCalendar(account, calendarId); 3144 } 3145 3146 /** 3147 * Tests correct behavior of Calendars.isPrimary column 3148 */ 3149 @MediumTest 3150 public void testCalendarIsPrimary() { 3151 String account = "ec_account"; 3152 int seed = 0; 3153 3154 // Clean up just in case 3155 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3156 3157 int isPrimary; 3158 Cursor cursor; 3159 ContentValues values = new ContentValues(); 3160 3161 final long calendarId = createAndVerifyCalendar(account, seed++, null); 3162 final Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId); 3163 3164 // verify when ownerAccount != account_name && isPrimary IS NULL 3165 cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null); 3166 cursor.moveToFirst(); 3167 isPrimary = cursor.getInt(0); 3168 cursor.close(); 3169 assertEquals("isPrimary should be 0 if ownerAccount != account_name", 0, isPrimary); 3170 3171 // verify when ownerAccount == account_name && isPrimary IS NULL 3172 values.clear(); 3173 values.put(Calendars.OWNER_ACCOUNT, account); 3174 mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null); 3175 cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null); 3176 cursor.moveToFirst(); 3177 isPrimary = cursor.getInt(0); 3178 cursor.close(); 3179 assertEquals("isPrimary should be 1 if ownerAccount == account_name", 1, isPrimary); 3180 3181 // verify isPrimary IS NOT NULL 3182 values.clear(); 3183 values.put(Calendars.IS_PRIMARY, SOME_ARBITRARY_INT); 3184 mContentResolver.update(uri, values, null, null); 3185 cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null); 3186 cursor.moveToFirst(); 3187 isPrimary = cursor.getInt(0); 3188 cursor.close(); 3189 assertEquals("isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isPrimary); 3190 3191 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3192 } 3193 3194 /** 3195 * Tests correct behavior of Events.isOrganizer column 3196 */ 3197 @MediumTest 3198 public void testEventsIsOrganizer() { 3199 String account = "ec_account"; 3200 int seed = 0; 3201 3202 // Clean up just in case 3203 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3204 3205 int isOrganizer; 3206 Cursor cursor; 3207 ContentValues values = new ContentValues(); 3208 3209 final long calendarId = createAndVerifyCalendar(account, seed++, null); 3210 final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null); 3211 final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); 3212 3213 // verify when ownerAccount != organizer && isOrganizer IS NULL 3214 cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null); 3215 cursor.moveToFirst(); 3216 isOrganizer = cursor.getInt(0); 3217 cursor.close(); 3218 assertEquals("isOrganizer should be 0 if ownerAccount != organizer", 0, isOrganizer); 3219 3220 // verify when ownerAccount == account_name && isOrganizer IS NULL 3221 values.clear(); 3222 values.put(Events.ORGANIZER, CalendarHelper.generateCalendarOwnerEmail(account)); 3223 mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null); 3224 cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null); 3225 cursor.moveToFirst(); 3226 isOrganizer = cursor.getInt(0); 3227 cursor.close(); 3228 assertEquals("isOrganizer should be 1 if ownerAccount == organizer", 1, isOrganizer); 3229 3230 // verify isOrganizer IS NOT NULL 3231 values.clear(); 3232 values.put(Events.IS_ORGANIZER, SOME_ARBITRARY_INT); 3233 mContentResolver.update(uri, values, null, null); 3234 cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null); 3235 cursor.moveToFirst(); 3236 isOrganizer = cursor.getInt(0); 3237 cursor.close(); 3238 assertEquals( 3239 "isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isOrganizer); 3240 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3241 } 3242 3243 /** 3244 * Tests correct behavior of Events.uid2445 column 3245 */ 3246 @MediumTest 3247 public void testEventsUid2445() { 3248 String account = "ec_account"; 3249 int seed = 0; 3250 3251 // Clean up just in case 3252 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3253 3254 final String uid = "uid_123"; 3255 Cursor cursor; 3256 ContentValues values = new ContentValues(); 3257 final long calendarId = createAndVerifyCalendar(account, seed++, null); 3258 final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null); 3259 final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); 3260 3261 // Verify default is null 3262 cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null); 3263 cursor.moveToFirst(); 3264 assertTrue(cursor.isNull(0)); 3265 cursor.close(); 3266 3267 // Write column value and read back 3268 values.clear(); 3269 values.put(Events.UID_2445, uid); 3270 mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null); 3271 cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null); 3272 cursor.moveToFirst(); 3273 assertFalse(cursor.isNull(0)); 3274 assertEquals("Column uid_2445 has unexpected value.", uid, cursor.getString(0)); 3275 3276 CalendarHelper.deleteCalendarByAccount(mContentResolver, account); 3277 } 3278 3279 /** 3280 * Acquires the set of instances that appear between the specified start and end points. 3281 * 3282 * @param timeZone Time zone to use when parsing startWhen and endWhen 3283 * @param startWhen Start date/time, in RFC 3339 format 3284 * @param endWhen End date/time, in RFC 3339 format 3285 * @param projection Array of desired column names 3286 * @return Cursor with instances (caller should close when done) 3287 */ 3288 private Cursor getInstances(String timeZone, String startWhen, String endWhen, 3289 String[] projection, long[] calendarIds) { 3290 Time startTime = new Time(timeZone); 3291 startTime.parse3339(startWhen); 3292 long startMillis = startTime.toMillis(false); 3293 3294 Time endTime = new Time(timeZone); 3295 endTime.parse3339(endWhen); 3296 long endMillis = endTime.toMillis(false); 3297 3298 // We want a list of instances that occur between the specified dates. Use the 3299 // "instances/when" URI. 3300 Uri uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI, 3301 startMillis + "/" + endMillis); 3302 3303 String where = null; 3304 for (int i = 0; i < calendarIds.length; i++) { 3305 if (i > 0) { 3306 where += " OR "; 3307 } else { 3308 where = ""; 3309 } 3310 where += (Instances.CALENDAR_ID + "=" + calendarIds[i]); 3311 } 3312 Cursor instances = mContentResolver.query(uri, projection, where, null, 3313 projection[0] + " ASC"); 3314 3315 return instances; 3316 } 3317 3318 /** 3319 * Acquires the set of instances that appear between the specified start and end points 3320 * that match the search terms. 3321 * 3322 * @param timeZone Time zone to use when parsing startWhen and endWhen 3323 * @param startWhen Start date/time, in RFC 3339 format 3324 * @param endWhen End date/time, in RFC 3339 format 3325 * @param search A collection of tokens to search for. The columns searched are 3326 * hard-coded in the provider (currently title, description, location, attendee 3327 * name, attendee email). 3328 * @param searchByDay If set, adjust start/end to calendar day boundaries. 3329 * @param projection Array of desired column names 3330 * @return Cursor with instances (caller should close when done) 3331 */ 3332 private Cursor getInstancesSearch(String timeZone, String startWhen, String endWhen, 3333 String search, boolean searchByDay, String[] projection, long[] calendarIds) { 3334 Time startTime = new Time(timeZone); 3335 startTime.parse3339(startWhen); 3336 long startMillis = startTime.toMillis(false); 3337 3338 Time endTime = new Time(timeZone); 3339 endTime.parse3339(endWhen); 3340 long endMillis = endTime.toMillis(false); 3341 3342 Uri uri; 3343 if (searchByDay) { 3344 // start/end are Julian day numbers rather than time in milliseconds 3345 int julianStart = Time.getJulianDay(startMillis, startTime.gmtoff); 3346 int julianEnd = Time.getJulianDay(endMillis, endTime.gmtoff); 3347 uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_BY_DAY_URI, 3348 julianStart + "/" + julianEnd + "/" + search); 3349 } else { 3350 uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_URI, 3351 startMillis + "/" + endMillis + "/" + search); 3352 } 3353 3354 String where = null; 3355 for (int i = 0; i < calendarIds.length; i++) { 3356 if (i > 0) { 3357 where += " OR "; 3358 } else { 3359 where = ""; 3360 } 3361 where += (Instances.CALENDAR_ID + "=" + calendarIds[i]); 3362 } 3363 // We want a list of instances that occur between the specified dates and that match 3364 // the search terms. 3365 3366 Cursor instances = mContentResolver.query(uri, projection, where, null, 3367 projection[0] + " ASC"); 3368 3369 return instances; 3370 } 3371 3372 /** debug -- dump instances cursor */ 3373 private static void dumpInstances(Cursor instances, String timeZone, String msg) { 3374 Log.d(TAG, "Instances (" + msg + ")"); 3375 3376 int posn = instances.getPosition(); 3377 instances.moveToPosition(-1); 3378 3379 //Log.d(TAG, "+++ instances has " + instances.getCount() + " rows, " + 3380 // instances.getColumnCount() + " columns"); 3381 while (instances.moveToNext()) { 3382 long beginMil = instances.getLong(0); 3383 Time beginT = new Time(timeZone); 3384 beginT.set(beginMil); 3385 String logMsg = "--> begin=" + beginT.format3339(false) + " (" + beginMil + ")"; 3386 for (int i = 2; i < instances.getColumnCount(); i++) { 3387 logMsg += " [" + instances.getString(i) + "]"; 3388 } 3389 Log.d(TAG, logMsg); 3390 } 3391 instances.moveToPosition(posn); 3392 } 3393 3394 3395 /** 3396 * Counts the number of instances that appear between the specified start and end times. 3397 */ 3398 private int getInstanceCount(String timeZone, String startWhen, String endWhen, 3399 long[] calendarIds) { 3400 Cursor instances = getInstances(timeZone, startWhen, endWhen, 3401 new String[] { Instances._ID }, calendarIds); 3402 int count = instances.getCount(); 3403 instances.close(); 3404 return count; 3405 } 3406 3407 /** 3408 * Deletes an event as app and sync adapter which removes it from the db and 3409 * verifies after each. 3410 * 3411 * @param eventUri The uri for the event to delete 3412 * @param accountName TODO 3413 */ 3414 private void removeAndVerifyEvent(Uri eventUri, ContentValues eventValues, String accountName) { 3415 // Delete event 3416 EventHelper.deleteEvent(mContentResolver, eventUri, eventValues); 3417 // Verify 3418 verifyEvent(eventValues, ContentUris.parseId(eventUri)); 3419 // Delete as sync adapter 3420 assertEquals(1, 3421 EventHelper.deleteEventAsSyncAdapter(mContentResolver, eventUri, accountName)); 3422 // Verify 3423 Cursor c = EventHelper.getEventByUri(mContentResolver, eventUri); 3424 assertEquals(0, c.getCount()); 3425 c.close(); 3426 } 3427 3428 /** 3429 * Creates an event on the given calendar and verifies it. 3430 * 3431 * @param account 3432 * @param seed 3433 * @param calendarId 3434 * @param asSyncAdapter 3435 * @param values optional pre created set of values; will have several new entries added 3436 * @return the _id for the new event 3437 */ 3438 private long createAndVerifyEvent(String account, int seed, long calendarId, 3439 boolean asSyncAdapter, ContentValues values) { 3440 // Create an event 3441 if (values == null) { 3442 values = EventHelper.getNewEventValues(account, seed, calendarId, asSyncAdapter); 3443 } 3444 Uri insertUri = Events.CONTENT_URI; 3445 if (asSyncAdapter) { 3446 insertUri = asSyncAdapter(insertUri, account, CTS_TEST_TYPE); 3447 } 3448 Uri uri = mContentResolver.insert(insertUri, values); 3449 assertNotNull(uri); 3450 3451 // Verify 3452 EventHelper.addDefaultReadOnlyValues(values, account, asSyncAdapter); 3453 long eventId = ContentUris.parseId(uri); 3454 assertTrue(eventId >= 0); 3455 3456 verifyEvent(values, eventId); 3457 return eventId; 3458 } 3459 3460 /** 3461 * Updates an event, and verifies that the updates took. 3462 */ 3463 private void updateAndVerifyEvent(String account, long calendarId, long eventId, 3464 boolean asSyncAdapter, ContentValues updateValues) { 3465 Uri uri = Uri.withAppendedPath(Events.CONTENT_URI, String.valueOf(eventId)); 3466 if (asSyncAdapter) { 3467 uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); 3468 } 3469 int count = mContentResolver.update(uri, updateValues, null, null); 3470 3471 // Verify 3472 assertEquals(1, count); 3473 verifyEvent(updateValues, eventId); 3474 } 3475 3476 /** 3477 * Creates an exception to a recurring event, and verifies it. 3478 * @param account The account to use. 3479 * @param originalEventId The ID of the original event. 3480 * @param values Values for the exception; must include originalInstanceTime. 3481 * @return The _id for the new event. 3482 */ 3483 private long createAndVerifyException(String account, long originalEventId, 3484 ContentValues values, boolean asSyncAdapter) { 3485 // Create the exception 3486 Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, 3487 String.valueOf(originalEventId)); 3488 if (asSyncAdapter) { 3489 uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); 3490 } 3491 Uri resultUri = mContentResolver.insert(uri, values); 3492 assertNotNull(resultUri); 3493 long eventId = ContentUris.parseId(resultUri); 3494 assertTrue(eventId >= 0); 3495 return eventId; 3496 } 3497 3498 /** 3499 * Deletes an exception to a recurring event. 3500 * @param account The account to use. 3501 * @param eventId The ID of the original recurring event. 3502 * @param excepId The ID of the exception event. 3503 * @return The number of rows deleted. 3504 */ 3505 private int deleteException(String account, long eventId, long excepId) { 3506 Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, 3507 eventId + "/" + excepId); 3508 uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); 3509 return mContentResolver.delete(uri, null, null); 3510 } 3511 3512 /** 3513 * Add some sample attendees to an event. 3514 */ 3515 private void addAttendees(String account, long eventId, int seed) { 3516 assertTrue(eventId >= 0); 3517 AttendeeHelper.addAttendee(mContentResolver, eventId, 3518 "Attender" + seed, 3519 CalendarHelper.generateCalendarOwnerEmail(account), 3520 Attendees.ATTENDEE_STATUS_ACCEPTED, 3521 Attendees.RELATIONSHIP_ORGANIZER, 3522 Attendees.TYPE_NONE); 3523 seed++; 3524 3525 AttendeeHelper.addAttendee(mContentResolver, eventId, 3526 "Attender" + seed, 3527 "attender" + seed + "@example.com", 3528 Attendees.ATTENDEE_STATUS_TENTATIVE, 3529 Attendees.RELATIONSHIP_NONE, 3530 Attendees.TYPE_NONE); 3531 } 3532 3533 /** 3534 * Add some sample reminders to an event. 3535 */ 3536 private void addReminders(String account, long eventId, int seed) { 3537 ReminderHelper.addReminder(mContentResolver, eventId, seed * 5, Reminders.METHOD_ALERT); 3538 } 3539 3540 /** 3541 * Creates and removes an event that covers a specific range of dates. Call this to 3542 * cause the provider to expand the CalendarMetaData min/max values to include the range. 3543 * Useful when you want to see the provider expand the instances as the events are added. 3544 */ 3545 private void expandInstanceRange(String account, long calendarId, String testStart, 3546 String testEnd, String timeZone) { 3547 int seed = 0; 3548 3549 // TODO: this should use an UNTIL rule based on testEnd, not a COUNT 3550 ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed, 3551 calendarId, true, testStart, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=100"); 3552 3553 /* 3554 * Some of the helper functions modify "eventValues", so we want to make sure we're 3555 * passing a copy of anything we want to re-use. 3556 */ 3557 long eventId = createAndVerifyEvent(account, seed, calendarId, true, 3558 new ContentValues(eventValues)); 3559 assertTrue(eventId >= 0); 3560 3561 String[] projection = { Instances.BEGIN, Instances.START_MINUTE }; 3562 Cursor instances = getInstances(timeZone, testStart, testEnd, projection, 3563 new long[] { calendarId }); 3564 if (DEBUG_RECURRENCE) { 3565 dumpInstances(instances, timeZone, "prep-create"); 3566 } 3567 assertEquals("initial recurrence instance count", 3, instances.getCount()); 3568 instances.close(); 3569 3570 Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); 3571 removeAndVerifyEvent(eventUri, new ContentValues(eventValues), account); 3572 3573 instances = getInstances(timeZone, testStart, testEnd, projection, 3574 new long[] { calendarId }); 3575 if (DEBUG_RECURRENCE) { 3576 dumpInstances(instances, timeZone, "prep-clear"); 3577 } 3578 assertEquals("initial recurrence instance count", 0, instances.getCount()); 3579 instances.close(); 3580 3581 } 3582 3583 /** 3584 * Inserts a new calendar with the given account and seed and verifies it. 3585 * 3586 * @param account The account to add the calendar to 3587 * @param seed A number to use to generate the values 3588 * @return the created calendar's id 3589 */ 3590 private long createAndVerifyCalendar(String account, int seed, ContentValues values) { 3591 // Create a calendar 3592 if (values == null) { 3593 values = CalendarHelper.getNewCalendarValues(account, seed); 3594 } 3595 Uri syncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE); 3596 Uri uri = mContentResolver.insert(syncUri, values); 3597 long calendarId = ContentUris.parseId(uri); 3598 assertTrue(calendarId >= 0); 3599 3600 verifyCalendar(account, values, calendarId, 1); 3601 return calendarId; 3602 } 3603 3604 /** 3605 * Deletes a given calendar and verifies no calendars remain on that 3606 * account. 3607 * 3608 * @param account 3609 * @param id 3610 */ 3611 private void removeAndVerifyCalendar(String account, long id) { 3612 // TODO Add code to delete as app and sync adapter and test both 3613 3614 // Delete 3615 assertEquals(1, CalendarHelper.deleteCalendarById(mContentResolver, id)); 3616 3617 // Verify 3618 Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account); 3619 assertEquals(0, c.getCount()); 3620 c.close(); 3621 } 3622 3623 /** 3624 * Check all the fields of a calendar contained in values + id. 3625 * 3626 * @param account the account of the calendar 3627 * @param values the values to check against the db 3628 * @param id the _id of the calendar 3629 * @param expectedCount the number of calendars expected on this account 3630 */ 3631 private void verifyCalendar(String account, ContentValues values, long id, int expectedCount) { 3632 // Verify 3633 Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account); 3634 assertEquals(expectedCount, c.getCount()); 3635 assertTrue(c.moveToFirst()); 3636 while (c.getLong(0) != id) { 3637 assertTrue(c.moveToNext()); 3638 } 3639 for (String key : values.keySet()) { 3640 int index = c.getColumnIndex(key); 3641 assertTrue("Key " + key + " not in projection", index >= 0); 3642 assertEquals(key, values.getAsString(key), c.getString(index)); 3643 } 3644 c.close(); 3645 } 3646 3647 /** 3648 * Creates a new _sync_state entry and verifies the contents. 3649 */ 3650 private long createAndVerifySyncState(String account, ContentValues values) { 3651 assertNotNull(values); 3652 Uri syncUri = asSyncAdapter(SyncState.CONTENT_URI, account, CTS_TEST_TYPE); 3653 Uri uri = mContentResolver.insert(syncUri, values); 3654 long syncStateId = ContentUris.parseId(uri); 3655 assertTrue(syncStateId >= 0); 3656 3657 verifySyncState(account, values, syncStateId); 3658 return syncStateId; 3659 3660 } 3661 3662 /** 3663 * Removes the _sync_state entry with the specified id, then verifies that it's gone. 3664 */ 3665 private void removeAndVerifySyncState(String account) { 3666 assertEquals(1, SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true)); 3667 3668 // Verify 3669 Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account); 3670 try { 3671 assertEquals(0, c.getCount()); 3672 } finally { 3673 if (c != null) { 3674 c.close(); 3675 } 3676 } 3677 } 3678 3679 /** 3680 * Check all the fields of a _sync_state entry contained in values + id. This assumes 3681 * a single _sync_state has been created on the given account. 3682 */ 3683 private void verifySyncState(String account, ContentValues values, long id) { 3684 // Verify 3685 Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account); 3686 try { 3687 assertEquals(1, c.getCount()); 3688 assertTrue(c.moveToFirst()); 3689 assertEquals(id, c.getLong(0)); 3690 for (String key : values.keySet()) { 3691 int index = c.getColumnIndex(key); 3692 if (key.equals(SyncState.DATA)) { 3693 // TODO: can't compare as string, so compare as byte[] 3694 } else { 3695 assertEquals(key, values.getAsString(key), c.getString(index)); 3696 } 3697 } 3698 } finally { 3699 if (c != null) { 3700 c.close(); 3701 } 3702 } 3703 } 3704 3705 3706 /** 3707 * Special version of the test runner that does some remote Emma coverage housekeeping. 3708 */ 3709 public static class CalendarEmmaTestRunner extends InstrumentationCtsTestRunner { 3710 private static final Uri EMMA_CONTENT_URI = 3711 Uri.parse("content://" + CalendarContract.AUTHORITY + "/emma"); 3712 private ContentResolver mContentResolver; 3713 3714 @Override 3715 public void onStart() { 3716 mContentResolver = getTargetContext().getContentResolver(); 3717 3718 ContentValues values = new ContentValues(); 3719 values.put("cmd", "start"); 3720 mContentResolver.insert(EMMA_CONTENT_URI, values); 3721 3722 super.onStart(); 3723 } 3724 3725 @Override 3726 public void finish(int resultCode, Bundle results) { 3727 ContentValues values = new ContentValues(); 3728 values.put("cmd", "stop"); 3729 values.put("outputFileName", 3730 Environment.getExternalStorageDirectory() + "/calendar-provider.ec"); 3731 mContentResolver.insert(EMMA_CONTENT_URI, values); 3732 super.finish(resultCode, results); 3733 } 3734 } 3735 } 3736