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