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 CalendarHelper()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 */ generateCalendarOwnerEmail(String account)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 */ getNewCalendarValues( String account, int seed)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 */ getUpdateCalendarValuesWithOriginal( ContentValues original, int seed)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 deleteCalendarById(ContentResolver resolver, long id)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 deleteCalendarByAccount(ContentResolver resolver, String account)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 getCalendarsByAccount(ContentResolver resolver, String account)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 SyncStateHelper()241 private SyncStateHelper() {} // do not instantiate 242 243 /** 244 * Creates a new set of values for creating a new _sync_state entry. 245 */ getNewSyncStateValues(String account)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 */ getSyncStateById(ContentResolver resolver, long id)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 */ getSyncStateByAccount(ContentResolver resolver, String account)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 */ deleteSyncStateById(ContentResolver resolver, long id)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 */ deleteSyncStateByAccount(ContentResolver resolver, String account, boolean asSyncAdapter)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 EventHelper()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 */ getNewEventValues( String account, int seed, long calendarId, boolean asSyncAdapter)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 */ getNewRecurringEventValues(String account, int seed, long calendarId, boolean asSyncAdapter, String startWhen, String duration, String rrule)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 */ getNewExceptionValues(long instanceStartMillis)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 getUpdateEventValuesWithOriginal(ContentValues original, int seed, boolean asSyncAdapter)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 addDefaultReadOnlyValues(ContentValues values, String account, boolean asSyncAdapter)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 */ generateDurationString(long durationMillis, boolean isAllDay)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 */ deleteEvent(ContentResolver resolver, Uri uri, ContentValues values)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 deleteEventAsSyncAdapter(ContentResolver resolver, Uri uri, String account)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 getEventsByAccount(ContentResolver resolver, String account)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 getEventByUri(ContentResolver resolver, Uri uri)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 */ lookupSelfAttendeeStatus(ContentResolver resolver, long eventId)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 */ lookupHasAlarm(ContentResolver resolver, long eventId)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 AttendeeHelper()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 */ addAttendee(ContentResolver resolver, long eventId, String name, String email, int status, int relationship, int type)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 */ findAttendeesByEmail(ContentResolver resolver, long eventId, String email)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 ColorHelper()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 */ addColor(ContentResolver resolver, String accountName, String accountType, String data, String index, int type, int color)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 */ findColorByIndex(ContentResolver resolver, String accountName, String accountType, String index)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 findColorsByAccount(ContentResolver resolver, String accountName, String accountType)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 */ addDefaultColorsToAccount(ContentResolver resolver, String accountName, String accountType)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 deleteColorsByAccount(ContentResolver resolver, String accountName, String accountType)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 ReminderHelper()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 */ addReminder(ContentResolver resolver, long eventId, int minutes, int method)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 */ findRemindersByEventId(ContentResolver resolver, long eventId)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 */ lookupMethod(ContentResolver resolver, long remId)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 ExtendedPropertiesHelper()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 */ addExtendedProperty(ContentResolver resolver, String account, long eventId, String name, String value)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 */ findExtendedPropertiesByEventId(ContentResolver resolver, long eventId)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 */ lookupValueByName(ContentResolver resolver, long eventId, String name)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 */ asSyncAdapter(Uri uri, String account, String accountType)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 */ getIntFromDatabase(ContentResolver resolver, Uri uri, long rowId, String columnName)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 setUp()916 protected void setUp() throws Exception { 917 super.setUp(); 918 mContentResolver = getInstrumentation().getTargetContext().getContentResolver(); 919 } 920 921 @MediumTest testCalendarCreationAndDeletion()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 testDefaultProjections()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 testEventsEntityQuery()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 testCalendarEntityQuery()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 testAttendees()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@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@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@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@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 testReminders()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 onReceive(Context context, Intent intent)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 */ testRemindersAlarm()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 testColorWriteRequirements()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 testCalendarColors()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 testEventColors()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 testExtendedProperties()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 CalendarEventHelper(String account, int seed)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 addEvent(String timeString, int timeZoneIndex, long duration)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 getCalendarId()1970 public long getCalendarId() { 1971 return mCalendarId; 1972 } 1973 } 1974 1975 /** 1976 * Test query to retrieve instances within a certain time interval. 1977 */ testWhenByDayQuery()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 verifyContentValuesAgainstCursor(List<ContentValues> cvs, Set<String> keys, Cursor cursor)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 timeInMillis(String timeString, int timeZoneIndex)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 getJulianDay(String timeString, int timeZoneIndex)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 testInstanceSearch()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 testCalendarUpdateAsApp()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 testSyncState()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 verifyEvent(ContentValues values, long eventId)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 testEventCreationAndDeletion()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 testEventUpdateAsApp()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 testBulkUpdate()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 testSyncOnlyInsertEnforcement()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 testRecurrence()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 testConversionToRecurring()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 testSingleRecurrenceExceptions()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-07-02T00:00:00"; 2520 String testEnd = "1999-08-04T23: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 testNonAdapterRecurrenceExceptions()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 testOutOfOrderRecurrenceExceptions()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 testForwardRecurrenceExceptions()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 testFullRecurrenceUpdate()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 testMultiRuleRecurrence()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 testBadRequests()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 testCalendarIsPrimary()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 testEventsIsOrganizer()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 testEventsUid2445()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 testMutatorSetCorrectly()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 */ getInstances(String timeZone, String startWhen, String endWhen, String[] projection, long[] calendarIds)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 */ getInstancesSearch(String timeZone, String startWhen, String endWhen, String search, boolean searchByDay, String[] projection, long[] calendarIds)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 */ dumpInstances(Cursor instances, String timeZone, String msg)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 */ getInstanceCount(String timeZone, String startWhen, String endWhen, long[] calendarIds)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 */ removeAndVerifyEvent(Uri eventUri, ContentValues eventValues, String accountName)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 */ createAndVerifyEvent(String account, int seed, long calendarId, boolean asSyncAdapter, ContentValues values)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 */ updateAndVerifyEvent(String account, long calendarId, long eventId, boolean asSyncAdapter, ContentValues updateValues)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 */ createAndVerifyException(String account, long originalEventId, ContentValues values, boolean asSyncAdapter)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 */ deleteException(String account, long eventId, long excepId)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 */ addAttendees(String account, long eventId, int seed)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 */ addReminders(String account, long eventId, int seed)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 */ expandInstanceRange(String account, long calendarId, String testStart, String testEnd, String timeZone)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 */ createAndVerifyCalendar(String account, int seed, ContentValues values)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 */ removeAndVerifyCalendar(String account, long id)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 */ verifyCalendar(String account, ContentValues values, long id, int expectedCount)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 */ createAndVerifySyncState(String account, ContentValues values)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 */ removeAndVerifySyncState(String account)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 */ verifySyncState(String account, ContentValues values, long id)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 onStart()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 finish(int resultCode, Bundle results)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