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