1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.calendar; 18 19 import android.content.ComponentName; 20 import android.content.ContentProvider; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.content.pm.PackageManager; 28 import android.content.pm.ProviderInfo; 29 import android.content.res.Resources; 30 import android.database.Cursor; 31 import android.database.MatrixCursor; 32 import android.database.sqlite.SQLiteDatabase; 33 import android.database.sqlite.SQLiteOpenHelper; 34 import android.net.Uri; 35 import android.os.UserHandle; 36 import android.provider.BaseColumns; 37 import android.provider.CalendarContract; 38 import android.provider.CalendarContract.Calendars; 39 import android.provider.CalendarContract.Colors; 40 import android.provider.CalendarContract.Events; 41 import android.provider.CalendarContract.Instances; 42 import android.test.AndroidTestCase; 43 import android.test.IsolatedContext; 44 import android.test.RenamingDelegatingContext; 45 import android.test.mock.MockContentResolver; 46 import android.test.mock.MockContext; 47 import android.text.TextUtils; 48 import android.text.format.DateUtils; 49 import android.util.Log; 50 51 import androidx.test.filters.SmallTest; 52 import androidx.test.filters.Suppress; 53 54 import com.android.calendarcommon2.Time; 55 import com.android.providers.calendar.enterprise.CrossProfileCalendarHelper; 56 57 import java.io.File; 58 import java.util.Arrays; 59 import java.util.HashMap; 60 import java.util.Map; 61 import java.util.Set; 62 import java.util.TimeZone; 63 64 /** 65 * Runs various tests on an isolated Calendar provider with its own database. 66 * 67 * You can run the tests with the following command line: 68 * 69 * adb shell am instrument 70 * -e debug false 71 * -w 72 * -e class com.android.providers.calendar.CalendarProvider2Test 73 * com.android.providers.calendar.tests/android.test.InstrumentationTestRunner 74 * 75 * This test no longer extends ProviderTestCase2 because it actually doesn't 76 * allow you to inject a custom context (which we needed to mock out the calls 77 * to start a service). We the next best thing, which is copy the relevant code 78 * from PTC2 and extend AndroidTestCase instead. 79 */ 80 // flaky test, add back to LargeTest when fixed - bug 2395696 81 // @LargeTest 82 public class CalendarProvider2Test extends AndroidTestCase { 83 static final String TAG = "calendar"; 84 85 private static final String DEFAULT_ACCOUNT_TYPE = "com.google"; 86 private static final String DEFAULT_ACCOUNT = "joe@joe.com"; 87 88 89 private static final String WHERE_CALENDARS_SELECTED = Calendars.VISIBLE + "=?"; 90 private static final String[] WHERE_CALENDARS_ARGS = { 91 "1" 92 }; 93 private static final String WHERE_COLOR_ACCOUNT_AND_INDEX = Colors.ACCOUNT_NAME + "=? AND " 94 + Colors.ACCOUNT_TYPE + "=? AND " + Colors.COLOR_KEY + "=?"; 95 private static final String DEFAULT_SORT_ORDER = "begin ASC"; 96 97 private CalendarProvider2ForTesting mProvider; 98 private CalendarProvider2ForTesting mWorkProfileProvider; 99 100 private SQLiteDatabase mDb; 101 private MetaData mMetaData; 102 private Context mContext; 103 private Context mWorkContext; 104 private MockContentResolver mResolver; 105 private Uri mEventsUri = Events.CONTENT_URI; 106 private Uri mCalendarsUri = Calendars.CONTENT_URI; 107 private int mCalendarId; 108 109 protected boolean mWipe = false; 110 protected boolean mForceDtend = false; 111 112 // We need a unique id to put in the _sync_id field so that we can create 113 // recurrence exceptions that refer to recurring events. 114 private int mGlobalSyncId = 1000; 115 private static final String CALENDAR_URL = 116 "http://www.google.com/calendar/feeds/joe%40joe.com/private/full"; 117 118 private static final String TIME_ZONE_AMERICA_ANCHORAGE = "America/Anchorage"; 119 private static final String TIME_ZONE_AMERICA_LOS_ANGELES = "America/Los_Angeles"; 120 private static final String DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES; 121 122 private static final String MOCK_TIME_ZONE_DATABASE_VERSION = "2010a"; 123 124 private static final long ONE_MINUTE_MILLIS = 60*1000; 125 private static final long ONE_HOUR_MILLIS = 3600*1000; 126 private static final long ONE_WEEK_MILLIS = 7 * 24 * 3600 * 1000; 127 128 private static final int WORK_PROFILE_USER_ID = 10; 129 private static final String WORK_PROFILE_AUTHORITY = String.format("%d@%s", 130 WORK_PROFILE_USER_ID, CalendarContract.AUTHORITY); 131 parseTimeStringToMillis(String timeStr, String timeZone)132 private static long parseTimeStringToMillis(String timeStr, String timeZone) { 133 Time time = new Time(timeZone); 134 time.parse3339(timeStr); 135 return time.toMillis(); 136 } 137 138 private static String WORK_DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES; 139 140 private static String WORK_CALENDAR_TITLE = "Calendar1"; 141 private static String WORK_CALENDAR_TITLE_STANDBY = "Calendar2"; 142 private static int WORK_CALENDAR_COLOR = 0xFFFF0000; 143 144 private static String WORK_EVENT_TITLE = "event_title1"; 145 private static String WORK_EVENT_TITLE_STANDBY = "event_title2"; 146 private static long WORK_EVENT_DTSTART = parseTimeStringToMillis( 147 "2018-05-01T00:00:00", WORK_DEFAULT_TIMEZONE); 148 private static long WORK_EVENT_DTEND = parseTimeStringToMillis( 149 "2018-05-01T20:00:00", WORK_DEFAULT_TIMEZONE); 150 private final long WORK_EVENT_DTSTART_STANDBY = parseTimeStringToMillis( 151 "2008-05-01T00:00:00", WORK_DEFAULT_TIMEZONE); 152 private final long WORK_EVENT_DTEND_STANDBY = parseTimeStringToMillis( 153 "2008-05-01T20:00:00", WORK_DEFAULT_TIMEZONE); 154 private static int WORK_EVENT_COLOR = 0xff123456; 155 private static String WORK_EVENT_LOCATION = "Work event location."; 156 private static String WORK_EVENT_DESCRIPTION = "This is a work event."; 157 158 /** 159 * We need a few more stub methods so that our tests can run 160 */ 161 protected class MockContext2 extends MockContext { 162 163 @Override getPackageName()164 public String getPackageName() { 165 return getContext().getPackageName(); 166 } 167 168 @Override getResources()169 public Resources getResources() { 170 return getContext().getResources(); 171 } 172 173 @Override getDir(String name, int mode)174 public File getDir(String name, int mode) { 175 // name the directory so the directory will be seperated from 176 // one created through the regular Context 177 return getContext().getDir("mockcontext2_" + name, mode); 178 } 179 180 @Override startService(Intent service)181 public ComponentName startService(Intent service) { 182 return null; 183 } 184 185 @Override stopService(Intent service)186 public boolean stopService(Intent service) { 187 return false; 188 } 189 190 @Override getPackageManager()191 public PackageManager getPackageManager() { 192 return getContext().getPackageManager(); 193 } 194 } 195 196 /** 197 * KeyValue is a simple class that stores a pair of strings representing 198 * a (key, value) pair. This is used for updating events. 199 */ 200 private class KeyValue { 201 String key; 202 String value; 203 KeyValue(String key, String value)204 public KeyValue(String key, String value) { 205 this.key = key; 206 this.value = value; 207 } 208 } 209 210 /** 211 * A generic command interface. This is used to support a sequence of 212 * commands that can create events, delete or update events, and then 213 * check that the state of the database is as expected. 214 */ 215 private interface Command { execute()216 public void execute(); 217 } 218 219 /** 220 * This is used to insert a new event into the database. The event is 221 * specified by its name (or "title"). All of the event fields (the 222 * start and end time, whether it is an all-day event, and so on) are 223 * stored in a separate table (the "mEvents" table). 224 */ 225 private class Insert implements Command { 226 EventInfo eventInfo; 227 Insert(String eventName)228 public Insert(String eventName) { 229 eventInfo = findEvent(eventName); 230 } 231 execute()232 public void execute() { 233 Log.i(TAG, "insert " + eventInfo.mTitle); 234 insertEvent(mCalendarId, eventInfo); 235 } 236 } 237 238 /** 239 * This is used to delete an event, specified by the event name. 240 */ 241 private class Delete implements Command { 242 String eventName; 243 String account; 244 String accountType; 245 int expected; 246 Delete(String eventName, int expected, String account, String accountType)247 public Delete(String eventName, int expected, String account, String accountType) { 248 this.eventName = eventName; 249 this.expected = expected; 250 this.account = account; 251 this.accountType = accountType; 252 } 253 execute()254 public void execute() { 255 Log.i(TAG, "delete " + eventName); 256 int rows = deleteMatchingEvents(eventName, account, accountType); 257 assertEquals(expected, rows); 258 } 259 } 260 261 /** 262 * This is used to update an event. The values to update are specified 263 * with an array of (key, value) pairs. Both the key and value are 264 * specified as strings. Event fields that are not really strings (such 265 * as DTSTART which is a long) should be converted to the appropriate type 266 * but that isn't supported yet. When needed, that can be added here 267 * by checking for specific keys and converting the associated values. 268 */ 269 private class Update implements Command { 270 String eventName; 271 KeyValue[] pairs; 272 Update(String eventName, KeyValue[] pairs)273 public Update(String eventName, KeyValue[] pairs) { 274 this.eventName = eventName; 275 this.pairs = pairs; 276 } 277 execute()278 public void execute() { 279 Log.i(TAG, "update " + eventName); 280 if (mWipe) { 281 // Wipe instance table so it will be regenerated 282 mMetaData.clearInstanceRange(); 283 } 284 ContentValues map = new ContentValues(); 285 for (KeyValue pair : pairs) { 286 String value = pair.value; 287 if (CalendarContract.Events.STATUS.equals(pair.key)) { 288 // Do type conversion for STATUS 289 map.put(pair.key, Integer.parseInt(value)); 290 } else { 291 map.put(pair.key, value); 292 } 293 } 294 if (map.size() == 1 && map.containsKey(Events.STATUS)) { 295 updateMatchingEventsStatusOnly(eventName, map); 296 } else { 297 updateMatchingEvents(eventName, map); 298 } 299 } 300 } 301 302 /** 303 * This command queries the number of events and compares it to the given 304 * expected value. 305 */ 306 private class QueryNumEvents implements Command { 307 int expected; 308 QueryNumEvents(int expected)309 public QueryNumEvents(int expected) { 310 this.expected = expected; 311 } 312 execute()313 public void execute() { 314 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 315 assertEquals(expected, cursor.getCount()); 316 cursor.close(); 317 } 318 } 319 320 321 /** 322 * This command dumps the list of events to the log for debugging. 323 */ 324 private class DumpEvents implements Command { 325 DumpEvents()326 public DumpEvents() { 327 } 328 execute()329 public void execute() { 330 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 331 dumpCursor(cursor); 332 cursor.close(); 333 } 334 } 335 336 /** 337 * This command dumps the list of instances to the log for debugging. 338 */ 339 private class DumpInstances implements Command { 340 long begin; 341 long end; 342 DumpInstances(String startDate, String endDate)343 public DumpInstances(String startDate, String endDate) { 344 Time time = new Time(DEFAULT_TIMEZONE); 345 time.parse3339(startDate); 346 begin = time.toMillis(); 347 time.parse3339(endDate); 348 end = time.toMillis(); 349 } 350 execute()351 public void execute() { 352 Cursor cursor = queryInstances(begin, end); 353 dumpCursor(cursor); 354 cursor.close(); 355 } 356 } 357 358 /** 359 * This command queries the number of instances and compares it to the given 360 * expected value. 361 */ 362 private class QueryNumInstances implements Command { 363 int expected; 364 long begin; 365 long end; 366 QueryNumInstances(String startDate, String endDate, int expected)367 public QueryNumInstances(String startDate, String endDate, int expected) { 368 Time time = new Time(DEFAULT_TIMEZONE); 369 time.parse3339(startDate); 370 begin = time.toMillis(); 371 time.parse3339(endDate); 372 end = time.toMillis(); 373 this.expected = expected; 374 } 375 execute()376 public void execute() { 377 Cursor cursor = queryInstances(begin, end); 378 assertEquals(expected, cursor.getCount()); 379 cursor.close(); 380 } 381 } 382 383 /** 384 * When this command runs it verifies that all of the instances in the 385 * given range match the expected instances (each instance is specified by 386 * a start date). 387 * If you just want to verify that an instance exists in a given date 388 * range, use {@link VerifyInstance} instead. 389 */ 390 private class VerifyAllInstances implements Command { 391 long[] instances; 392 long begin; 393 long end; 394 VerifyAllInstances(String startDate, String endDate, String[] dates)395 public VerifyAllInstances(String startDate, String endDate, String[] dates) { 396 Time time = new Time(DEFAULT_TIMEZONE); 397 time.parse3339(startDate); 398 begin = time.toMillis(); 399 time.parse3339(endDate); 400 end = time.toMillis(); 401 402 if (dates == null) { 403 return; 404 } 405 406 // Convert all the instance date strings to UTC milliseconds 407 int len = dates.length; 408 this.instances = new long[len]; 409 int index = 0; 410 for (String instance : dates) { 411 time.parse3339(instance); 412 this.instances[index++] = time.toMillis(); 413 } 414 } 415 execute()416 public void execute() { 417 Cursor cursor = queryInstances(begin, end); 418 int len = 0; 419 if (instances != null) { 420 len = instances.length; 421 } 422 if (len != cursor.getCount()) { 423 dumpCursor(cursor); 424 } 425 assertEquals("number of instances don't match", len, cursor.getCount()); 426 427 if (instances == null) { 428 return; 429 } 430 431 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 432 while (cursor.moveToNext()) { 433 long begin = cursor.getLong(beginColumn); 434 435 // Search the list of expected instances for a matching start 436 // time. 437 boolean found = false; 438 for (long instance : instances) { 439 if (instance == begin) { 440 found = true; 441 break; 442 } 443 } 444 if (!found) { 445 int titleColumn = cursor.getColumnIndex(Events.TITLE); 446 int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY); 447 448 String title = cursor.getString(titleColumn); 449 boolean allDay = cursor.getInt(allDayColumn) != 0; 450 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | 451 DateUtils.FORMAT_24HOUR; 452 if (allDay) { 453 flags |= DateUtils.FORMAT_UTC; 454 } else { 455 flags |= DateUtils.FORMAT_SHOW_TIME; 456 } 457 String date = DateUtils.formatDateRange(mContext, begin, begin, flags); 458 String mesg = String.format("Test failed!" 459 + " unexpected instance (\"%s\") at %s", 460 title, date); 461 Log.e(TAG, mesg); 462 } 463 if (!found) { 464 dumpCursor(cursor); 465 } 466 assertTrue(found); 467 } 468 cursor.close(); 469 } 470 } 471 472 /** 473 * When this command runs it verifies that the given instance exists in 474 * the given date range. 475 */ 476 private class VerifyInstance implements Command { 477 long instance; 478 boolean allDay; 479 long begin; 480 long end; 481 482 /** 483 * Creates a command to check that the given range [startDate,endDate] 484 * contains a specific instance of an event (specified by "date"). 485 * 486 * @param startDate the beginning of the date range 487 * @param endDate the end of the date range 488 * @param date the date or date-time string of an event instance 489 */ VerifyInstance(String startDate, String endDate, String date)490 public VerifyInstance(String startDate, String endDate, String date) { 491 Time time = new Time(DEFAULT_TIMEZONE); 492 time.parse3339(startDate); 493 begin = time.toMillis(); 494 time.parse3339(endDate); 495 end = time.toMillis(); 496 497 // Convert the instance date string to UTC milliseconds 498 time.parse3339(date); 499 allDay = time.isAllDay(); 500 instance = time.toMillis(); 501 } 502 execute()503 public void execute() { 504 Cursor cursor = queryInstances(begin, end); 505 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 506 boolean found = false; 507 while (cursor.moveToNext()) { 508 long begin = cursor.getLong(beginColumn); 509 510 if (instance == begin) { 511 found = true; 512 break; 513 } 514 } 515 if (!found) { 516 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE; 517 if (allDay) { 518 flags |= DateUtils.FORMAT_UTC; 519 } else { 520 flags |= DateUtils.FORMAT_SHOW_TIME; 521 } 522 String date = DateUtils.formatDateRange(mContext, instance, instance, flags); 523 String mesg = String.format("Test failed!" 524 + " cannot find instance at %s", 525 date); 526 Log.e(TAG, mesg); 527 } 528 assertTrue(found); 529 cursor.close(); 530 } 531 } 532 533 /** 534 * This class stores all the useful information about an event. 535 */ 536 private class EventInfo { 537 String mTitle; 538 String mDescription; 539 String mTimezone; 540 boolean mAllDay; 541 long mDtstart; 542 long mDtend; 543 String mRrule; 544 String mDuration; 545 String mOriginalTitle; 546 long mOriginalInstance; 547 int mSyncId; 548 String mCustomAppPackage; 549 String mCustomAppUri; 550 String mUid2445; 551 552 // Constructor for normal events, using the default timezone EventInfo(String title, String startDate, String endDate, boolean allDay)553 public EventInfo(String title, String startDate, String endDate, 554 boolean allDay) { 555 init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE); 556 } 557 558 // Constructor for normal events, specifying the timezone EventInfo(String title, String startDate, String endDate, boolean allDay, String timezone)559 public EventInfo(String title, String startDate, String endDate, 560 boolean allDay, String timezone) { 561 init(title, startDate, endDate, allDay, timezone); 562 } 563 init(String title, String startDate, String endDate, boolean allDay, String timezone)564 public void init(String title, String startDate, String endDate, 565 boolean allDay, String timezone) { 566 mTitle = title; 567 Time time = new Time(); 568 if (allDay) { 569 time.setTimezone(Time.TIMEZONE_UTC); 570 } else if (timezone != null) { 571 time.setTimezone(timezone); 572 } 573 mTimezone = time.getTimezone(); 574 time.parse3339(startDate); 575 mDtstart = time.toMillis(); 576 time.parse3339(endDate); 577 mDtend = time.toMillis(); 578 mDuration = null; 579 mRrule = null; 580 mAllDay = allDay; 581 mCustomAppPackage = "CustomAppPackage-" + mTitle; 582 mCustomAppUri = "CustomAppUri-" + mTitle; 583 mUid2445 = null; 584 } 585 586 // Constructor for repeating events, using the default timezone EventInfo(String title, String description, String startDate, String endDate, String rrule, boolean allDay)587 public EventInfo(String title, String description, String startDate, String endDate, 588 String rrule, boolean allDay) { 589 init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE); 590 } 591 592 // Constructor for repeating events, specifying the timezone EventInfo(String title, String description, String startDate, String endDate, String rrule, boolean allDay, String timezone)593 public EventInfo(String title, String description, String startDate, String endDate, 594 String rrule, boolean allDay, String timezone) { 595 init(title, description, startDate, endDate, rrule, allDay, timezone); 596 } 597 init(String title, String description, String startDate, String endDate, String rrule, boolean allDay, String timezone)598 public void init(String title, String description, String startDate, String endDate, 599 String rrule, boolean allDay, String timezone) { 600 mTitle = title; 601 mDescription = description; 602 Time time = new Time(); 603 if (allDay) { 604 time.setTimezone(Time.TIMEZONE_UTC); 605 } else if (timezone != null) { 606 time.setTimezone(timezone); 607 } 608 mTimezone = time.getTimezone(); 609 time.parse3339(startDate); 610 mDtstart = time.toMillis(); 611 if (endDate != null) { 612 time.parse3339(endDate); 613 mDtend = time.toMillis(); 614 } 615 if (allDay) { 616 long days = 1; 617 if (endDate != null) { 618 days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS; 619 } 620 mDuration = "P" + days + "D"; 621 } else { 622 long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS; 623 mDuration = "P" + seconds + "S"; 624 } 625 mRrule = rrule; 626 mAllDay = allDay; 627 } 628 629 // Constructor for recurrence exceptions, using the default timezone EventInfo(String originalTitle, String originalInstance, String title, String description, String startDate, String endDate, boolean allDay, String customPackageName, String customPackageUri, String mUid2445)630 public EventInfo(String originalTitle, String originalInstance, String title, 631 String description, String startDate, String endDate, boolean allDay, 632 String customPackageName, String customPackageUri, String mUid2445) { 633 init(originalTitle, originalInstance, 634 title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE, 635 customPackageName, customPackageUri, mUid2445); 636 } 637 init(String originalTitle, String originalInstance, String title, String description, String startDate, String endDate, boolean allDay, String timezone, String customPackageName, String customPackageUri, String uid2445)638 public void init(String originalTitle, String originalInstance, 639 String title, String description, String startDate, String endDate, 640 boolean allDay, String timezone, String customPackageName, 641 String customPackageUri, String uid2445) { 642 mOriginalTitle = originalTitle; 643 Time time = new Time(timezone); 644 time.parse3339(originalInstance); 645 mOriginalInstance = time.toMillis(); 646 mCustomAppPackage = customPackageName; 647 mCustomAppUri = customPackageUri; 648 mUid2445 = uid2445; 649 init(title, description, startDate, endDate, null /* rrule */, allDay, timezone); 650 } 651 } 652 653 private class InstanceInfo { 654 EventInfo mEvent; 655 long mBegin; 656 long mEnd; 657 int mExpectedOccurrences; 658 InstanceInfo(String eventName, String startDate, String endDate, int expected)659 public InstanceInfo(String eventName, String startDate, String endDate, int expected) { 660 // Find the test index that contains the given event name 661 mEvent = findEvent(eventName); 662 Time time = new Time(mEvent.mTimezone); 663 time.parse3339(startDate); 664 mBegin = time.toMillis(); 665 time.parse3339(endDate); 666 mEnd = time.toMillis(); 667 mExpectedOccurrences = expected; 668 } 669 } 670 671 /** 672 * This is the main table of events. The events in this table are 673 * referred to by name in other places. 674 */ 675 private EventInfo[] mEvents = { 676 new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false), 677 new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false), 678 new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false), 679 new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true), 680 new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true), 681 new EventInfo("daily0", "daily from 5/1/2008 12am to 1am", 682 "2008-05-01T00:00:00", "2008-05-01T01:00:00", 683 "FREQ=DAILY;WKST=SU", false), 684 new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am", 685 "2008-05-01T08:30:00", "2008-05-01T09:30:00", 686 "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false), 687 new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am", 688 "2008-05-01T08:45:00", "2008-05-01T09:15:00", 689 "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false), 690 new EventInfo("allday daily0", "all-day daily from 5/1/2008", 691 "2008-05-01", null, 692 "FREQ=DAILY;WKST=SU", true), 693 new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008", 694 "2008-05-01", null, 695 "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true), 696 new EventInfo("allday weekly0", "all-day weekly from 5/1/2008", 697 "2008-05-01", null, 698 "FREQ=WEEKLY;WKST=SU", true), 699 new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008", 700 "2008-05-01", "2008-05-03", 701 "FREQ=WEEKLY;WKST=SU", true), 702 new EventInfo("allday yearly0", "all-day yearly on 5/1/2008", 703 "2008-05-01T", null, 704 "FREQ=YEARLY;WKST=SU", true), 705 new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm", 706 "2008-05-06T13:00:00", "2008-05-06T14:00:00", 707 "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false), 708 new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm", 709 "2008-05-06T14:30:00", "2008-05-06T15:30:00", 710 "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false), 711 new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm", 712 "2008-05-20T15:00:00", "2008-05-20T16:00:00", 713 "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false), 714 new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am", 715 "2008-05-01T00:00:00", "2008-05-01T00:10:00", 716 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false), 717 new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight", 718 "2008-05-31T23:00:00", "2008-06-01T00:00:00", 719 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false), 720 new EventInfo("daily0", "2008-05-01T00:00:00", 721 "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am", 722 "2008-05-01T02:00:00", "2008-05-01T01:03:00", false, "AppPkg1", "AppUri1", 723 "uid2445-1"), 724 new EventInfo("daily0", "2008-05-03T00:00:00", 725 "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am", 726 "2008-05-03T02:00:00", "2008-05-03T01:03:00", false, "AppPkg2", "AppUri2", 727 null), 728 new EventInfo("daily0", "2008-05-02T00:00:00", 729 "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008", 730 "2008-01-02T00:00:00", "2008-01-02T01:00:00", false, "AppPkg3", "AppUri3", 731 "12345@uid2445"), 732 new EventInfo("weekly0", "2008-05-13T13:00:00", 733 "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm", 734 "2008-12-11T13:00:00", "2008-12-11T14:00:00", false, "AppPkg4", "AppUri4", 735 null), 736 new EventInfo("weekly0", "2008-05-13T13:00:00", 737 "cancel0", "weekly0 exception for 5/13/2008 1pm", 738 "2008-05-13T13:00:00", "2008-05-13T14:00:00", false, "AppPkg5", "AppUri5", 739 null), 740 new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm", 741 "2008-05-01T13:00:00", "2008-05-01T14:00:00", 742 "FREQ=YEARLY;WKST=SU", false), 743 }; 744 745 /** 746 * This table is used to verify the events generated by mEvents. It checks that the 747 * number of instances within a given range matches the expected number 748 * of instances. 749 */ 750 private InstanceInfo[] mInstanceRanges = { 751 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1), 752 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 753 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2), 754 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2), 755 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 756 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1), 757 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2), 758 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31), 759 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32), 760 761 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 762 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2), 763 764 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 765 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3), 766 767 new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7), 768 new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3), 769 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1), 770 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2), 771 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5), 772 new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5), 773 new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1), 774 new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2), 775 776 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 777 new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 778 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4), 779 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8), 780 781 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 782 new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 783 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2), 784 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4), 785 786 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0), 787 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1), 788 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0), 789 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0), 790 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1), 791 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 792 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2), 793 794 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 795 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 796 new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1), 797 new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0), 798 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2), 799 800 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0), 801 new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1), 802 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1), 803 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2), 804 805 new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1), 806 new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2), 807 }; 808 809 /** 810 * This sequence of commands inserts and deletes some events. 811 */ 812 private Command[] mNormalInsertDelete = { 813 new Insert("normal0"), 814 new Insert("normal1"), 815 new Insert("normal2"), 816 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3), 817 new Delete("normal1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 818 new QueryNumEvents(2), 819 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2), 820 new Delete("normal1", 0, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 821 new Delete("normal2", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 822 new QueryNumEvents(1), 823 new Delete("normal0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 824 new QueryNumEvents(0), 825 }; 826 827 /** 828 * This sequence of commands inserts and deletes some all-day events. 829 */ 830 private Command[] mAlldayInsertDelete = { 831 new Insert("allday0"), 832 new Insert("allday1"), 833 new QueryNumEvents(2), 834 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0), 835 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2), 836 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 837 new Delete("allday0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 838 new QueryNumEvents(1), 839 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 840 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 841 new Delete("allday1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 842 new QueryNumEvents(0), 843 }; 844 845 /** 846 * This sequence of commands inserts and deletes some repeating events. 847 */ 848 private Command[] mRecurringInsertDelete = { 849 new Insert("daily0"), 850 new Insert("daily1"), 851 new QueryNumEvents(2), 852 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3), 853 new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2), 854 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6), 855 new Delete("daily1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 856 new QueryNumEvents(1), 857 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2), 858 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4), 859 new Delete("daily0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 860 new QueryNumEvents(0), 861 }; 862 863 /** 864 * This sequence of commands creates a recurring event with a recurrence 865 * exception that moves an event outside the expansion window. It checks that the 866 * recurrence exception does not occur in the Instances database table. 867 * Bug 1642665 868 */ 869 private Command[] mExceptionWithMovedRecurrence = { 870 new Insert("daily0"), 871 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 872 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 873 "2008-05-03T00:00:00", }), 874 new Insert("except2"), 875 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 876 new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}), 877 }; 878 879 /** 880 * This sequence of commands deletes (cancels) one instance of a recurrence. 881 */ 882 private Command[] mCancelInstance = { 883 new Insert("weekly0"), 884 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 885 new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00", 886 "2008-05-20T13:00:00", }), 887 new Insert("cancel0"), 888 new Update("cancel0", new KeyValue[] { 889 new KeyValue(CalendarContract.Events.STATUS, 890 Integer.toString(CalendarContract.Events.STATUS_CANCELED)), 891 }), 892 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 893 new String[] {"2008-05-06T13:00:00", 894 "2008-05-20T13:00:00", }), 895 }; 896 /** 897 * This sequence of commands creates a recurring event with a recurrence 898 * exception that moves an event from outside the expansion window into the 899 * expansion window. 900 */ 901 private Command[] mExceptionWithMovedRecurrence2 = { 902 new Insert("weekly0"), 903 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 904 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 905 "2008-12-16T13:00:00", }), 906 new Insert("except3"), 907 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 908 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 909 "2008-12-11T13:00:00", "2008-12-16T13:00:00", }), 910 }; 911 /** 912 * This sequence of commands creates a recurring event with a recurrence 913 * exception and then changes the end time of the recurring event. It then 914 * checks that the recurrence exception does not occur in the Instances 915 * database table. 916 */ 917 private Command[] 918 mExceptionWithTruncatedRecurrence = { 919 new Insert("daily0"), 920 // Verify 4 occurrences of the "daily0" repeating event 921 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 922 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 923 "2008-05-03T00:00:00", "2008-05-04T00:00:00"}), 924 new Insert("except1"), 925 new QueryNumEvents(2), 926 927 // Verify that one of the 4 occurrences has its start time changed 928 // so that it now matches the recurrence exception. 929 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 930 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 931 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 932 933 // Change the end time of "daily0" but it still includes the 934 // recurrence exception. 935 new Update("daily0", new KeyValue[] { 936 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"), 937 }), 938 939 // Verify that the recurrence exception is still there 940 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 941 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 942 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 943 // This time change the end time of "daily0" so that it excludes 944 // the recurrence exception. 945 new Update("daily0", new KeyValue[] { 946 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"), 947 }), 948 // The server will cancel the out-of-range exception. 949 // It would be nice for the provider to handle this automatically, 950 // but for now simulate the server-side cancel. 951 new Update("except1", new KeyValue[] { 952 new KeyValue(CalendarContract.Events.STATUS, 953 Integer.toString(CalendarContract.Events.STATUS_CANCELED)), 954 }), 955 // Verify that the recurrence exception does not appear. 956 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 957 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}), 958 }; 959 960 /** 961 * Bug 135848. Ensure that a recurrence exception is displayed even if the recurrence 962 * is not present. 963 */ 964 private Command[] mExceptionWithNoRecurrence = { 965 new Insert("except0"), 966 new QueryNumEvents(1), 967 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 968 new String[] {"2008-05-01T02:00:00"}), 969 }; 970 findEvent(String name)971 private EventInfo findEvent(String name) { 972 int len = mEvents.length; 973 for (int ii = 0; ii < len; ii++) { 974 EventInfo event = mEvents[ii]; 975 if (name.equals(event.mTitle)) { 976 return event; 977 } 978 } 979 return null; 980 } 981 982 @Override setUp()983 protected void setUp() throws Exception { 984 super.setUp(); 985 // This code here is the code that was originally in ProviderTestCase2 986 mResolver = new MockContentResolver(); 987 988 final String filenamePrefix = "test."; 989 RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext( 990 new MockContext2(), // The context that most methods are delegated to 991 getContext(), // The context that file methods are delegated to 992 filenamePrefix) { 993 @Override 994 public SharedPreferences getSharedPreferences(String name, int mode) { 995 return getContext().getSharedPreferences(name, mode); 996 } 997 }; 998 mContext = new IsolatedContext(mResolver, targetContextWrapper) { 999 @Override 1000 public Object getSystemService(String name) { 1001 switch (name) { 1002 case Context.POWER_SERVICE: 1003 case Context.USER_SERVICE: 1004 return getContext().getSystemService(name); 1005 default: 1006 return super.getSystemService(name); 1007 } 1008 } 1009 }; 1010 1011 mWorkContext = new IsolatedContext(mResolver, targetContextWrapper) { 1012 @Override 1013 public int getUserId() { 1014 return WORK_PROFILE_USER_ID; 1015 } 1016 1017 @Override 1018 public Object getSystemService(String name) { 1019 switch (name) { 1020 case Context.POWER_SERVICE: 1021 case Context.USER_SERVICE: 1022 return getContext().getSystemService(name); 1023 default: 1024 return super.getSystemService(name); 1025 } 1026 } 1027 }; 1028 1029 mProvider = new CalendarProvider2ForTesting() { 1030 @Override 1031 protected int getWorkProfileUserId() { 1032 return WORK_PROFILE_USER_ID; 1033 } 1034 1035 @Override 1036 protected int getParentUserId() { 1037 return UserHandle.USER_NULL; 1038 } 1039 1040 @Override 1041 protected void initCrossProfileCalendarHelper() { 1042 mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext); 1043 } 1044 }; 1045 ProviderInfo info = new ProviderInfo(); 1046 info.authority = CalendarContract.AUTHORITY; 1047 mProvider.attachInfoForTesting(mContext, info); 1048 1049 mWorkProfileProvider = new CalendarProvider2ForTesting() { 1050 @Override 1051 protected int getWorkProfileUserId() { 1052 return UserHandle.USER_NULL; 1053 } 1054 1055 @Override 1056 protected int getParentUserId() { 1057 return UserHandle.myUserId(); 1058 } 1059 1060 @Override 1061 protected void initCrossProfileCalendarHelper() { 1062 mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext); 1063 } 1064 }; 1065 ProviderInfo workProviderInfo = new ProviderInfo(); 1066 workProviderInfo.authority = WORK_PROFILE_AUTHORITY; 1067 mWorkProfileProvider.attachInfoForTesting(mWorkContext, info); 1068 1069 mResolver.addProvider(CalendarContract.AUTHORITY, mProvider); 1070 mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds")); 1071 mResolver.addProvider("sync", new MockProvider("sync")); 1072 mResolver.addProvider(WORK_PROFILE_AUTHORITY, mWorkProfileProvider); 1073 1074 mMetaData = getProvider().mMetaData; 1075 mForceDtend = false; 1076 1077 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 1078 mDb = helper.getWritableDatabase(); 1079 wipeAndInitData(helper, mDb); 1080 } 1081 1082 @Override tearDown()1083 protected void tearDown() throws Exception { 1084 try { 1085 mDb.close(); 1086 mDb = null; 1087 getProvider().getDatabaseHelper().close(); 1088 } catch (IllegalStateException e) { 1089 e.printStackTrace(); 1090 } 1091 super.tearDown(); 1092 } 1093 wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db)1094 public void wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db) 1095 throws CalendarCache.CacheException { 1096 db.beginTransaction(); 1097 1098 // Clean tables 1099 db.delete("Calendars", null, null); 1100 db.delete("Events", null, null); 1101 db.delete("EventsRawTimes", null, null); 1102 db.delete("Instances", null, null); 1103 db.delete("CalendarMetaData", null, null); 1104 db.delete("CalendarCache", null, null); 1105 db.delete("Attendees", null, null); 1106 db.delete("Reminders", null, null); 1107 db.delete("CalendarAlerts", null, null); 1108 db.delete("ExtendedProperties", null, null); 1109 1110 // Set CalendarCache data 1111 initCalendarCacheLocked(helper, db); 1112 1113 // set CalendarMetaData data 1114 long now = System.currentTimeMillis(); 1115 ContentValues values = new ContentValues(); 1116 values.put("localTimezone", "America/Los_Angeles"); 1117 values.put("minInstance", 1207008000000L); // 1st April 2008 1118 values.put("maxInstance", now + ONE_WEEK_MILLIS); 1119 db.insert("CalendarMetaData", null, values); 1120 1121 db.setTransactionSuccessful(); 1122 db.endTransaction(); 1123 } 1124 initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db)1125 private void initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db) 1126 throws CalendarCache.CacheException { 1127 CalendarCache cache = new CalendarCache(helper); 1128 1129 String localTimezone = TimeZone.getDefault().getID(); 1130 1131 // Set initial values 1132 cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_DATABASE_VERSION, "2010k"); 1133 cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_TYPE, CalendarCache.TIMEZONE_TYPE_AUTO); 1134 cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES, localTimezone); 1135 cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS, localTimezone); 1136 } 1137 getProvider()1138 protected CalendarProvider2ForTesting getProvider() { 1139 return mProvider; 1140 } 1141 1142 /** 1143 * Dumps the contents of the given cursor to the log. For debugging. 1144 * @param cursor the database cursor 1145 */ dumpCursor(Cursor cursor)1146 private void dumpCursor(Cursor cursor) { 1147 cursor.moveToPosition(-1); 1148 String[] cols = cursor.getColumnNames(); 1149 1150 Log.i(TAG, "dumpCursor() count: " + cursor.getCount()); 1151 int index = 0; 1152 while (cursor.moveToNext()) { 1153 Log.i(TAG, index + " {"); 1154 for (int i = 0; i < cols.length; i++) { 1155 Log.i(TAG, " " + cols[i] + '=' + cursor.getString(i)); 1156 } 1157 Log.i(TAG, "}"); 1158 index += 1; 1159 } 1160 cursor.moveToPosition(-1); 1161 } 1162 insertCal(String name, String timezone)1163 private int insertCal(String name, String timezone) { 1164 return insertCal(name, timezone, DEFAULT_ACCOUNT); 1165 } 1166 1167 /** 1168 * Creates a new calendar, with the provided name, time zone, and account name. 1169 * 1170 * @return the new calendar's _ID value 1171 */ insertCal(String name, String timezone, String account)1172 private int insertCal(String name, String timezone, String account) { 1173 ContentValues m = new ContentValues(); 1174 m.put(Calendars.NAME, name); 1175 m.put(Calendars.CALENDAR_DISPLAY_NAME, name); 1176 m.put(Calendars.CALENDAR_COLOR, 0xff123456); 1177 m.put(Calendars.CALENDAR_TIME_ZONE, timezone); 1178 m.put(Calendars.VISIBLE, 1); 1179 m.put(Calendars.CAL_SYNC1, CALENDAR_URL); 1180 m.put(Calendars.OWNER_ACCOUNT, account); 1181 m.put(Calendars.ACCOUNT_NAME, account); 1182 m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); 1183 m.put(Calendars.SYNC_EVENTS, 1); 1184 1185 Uri url = mResolver.insert( 1186 addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m); 1187 String id = url.getLastPathSegment(); 1188 return Integer.parseInt(id); 1189 } 1190 1191 /** 1192 * Creates a new calendar, with the provided name, time zone, and account name, but an empty 1193 * owner account, which makes this calendar non-primary calendar. 1194 * 1195 * @return the new calendar's _ID value 1196 */ insertNonPrimaryCal(String name, String timezone, String account)1197 private int insertNonPrimaryCal(String name, String timezone, String account) { 1198 ContentValues m = new ContentValues(); 1199 m.put(Calendars.NAME, name); 1200 m.put(Calendars.CALENDAR_DISPLAY_NAME, name); 1201 m.put(Calendars.CALENDAR_COLOR, 0xff123456); 1202 m.put(Calendars.CALENDAR_TIME_ZONE, timezone); 1203 m.put(Calendars.VISIBLE, 1); 1204 m.put(Calendars.CAL_SYNC1, CALENDAR_URL); 1205 m.put(Calendars.OWNER_ACCOUNT, ""); 1206 m.put(Calendars.ACCOUNT_NAME, account); 1207 m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); 1208 m.put(Calendars.SYNC_EVENTS, 1); 1209 1210 Uri url = mResolver.insert( 1211 addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m); 1212 String id = url.getLastPathSegment(); 1213 return Integer.parseInt(id); 1214 } 1215 obsToString(Object... objs)1216 private String obsToString(Object... objs) { 1217 StringBuilder bob = new StringBuilder(); 1218 1219 for (Object obj : objs) { 1220 bob.append(obj.toString()); 1221 bob.append('#'); 1222 } 1223 1224 return bob.toString(); 1225 } 1226 insertColor(long colorType, String colorKey, long color)1227 private Uri insertColor(long colorType, String colorKey, long color) { 1228 ContentValues m = new ContentValues(); 1229 m.put(Colors.ACCOUNT_NAME, DEFAULT_ACCOUNT); 1230 m.put(Colors.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); 1231 m.put(Colors.DATA, obsToString(colorType, colorKey, color)); 1232 m.put(Colors.COLOR_TYPE, colorType); 1233 m.put(Colors.COLOR_KEY, colorKey); 1234 m.put(Colors.COLOR, color); 1235 1236 Uri uri = CalendarContract.Colors.CONTENT_URI; 1237 1238 return mResolver.insert(addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), m); 1239 } 1240 updateAndCheckColor(long colorId, long colorType, String colorKey, long color)1241 private void updateAndCheckColor(long colorId, long colorType, String colorKey, long color) { 1242 1243 Uri uri = CalendarContract.Colors.CONTENT_URI; 1244 1245 final String where = Colors.ACCOUNT_NAME + "=? AND " + Colors.ACCOUNT_TYPE + "=? AND " 1246 + Colors.COLOR_TYPE + "=? AND " + Colors.COLOR_KEY + "=?"; 1247 1248 String[] selectionArgs = new String[] { 1249 DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, Long.toString(colorType), colorKey 1250 }; 1251 1252 ContentValues cv = new ContentValues(); 1253 cv.put(Colors.COLOR, color); 1254 cv.put(Colors.DATA, obsToString(colorType, colorKey, color)); 1255 1256 int count = mResolver.update( 1257 addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), cv, where, 1258 selectionArgs); 1259 1260 checkColor(colorId, colorType, colorKey, color); 1261 1262 assertEquals(1, count); 1263 } 1264 1265 /** 1266 * Constructs a URI from a base URI (e.g. "content://com.android.calendar/calendars"), 1267 * an account name, and an account type. 1268 */ addSyncQueryParams(Uri uri, String account, String accountType)1269 private Uri addSyncQueryParams(Uri uri, String account, String accountType) { 1270 return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") 1271 .appendQueryParameter(Calendars.ACCOUNT_NAME, account) 1272 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); 1273 } 1274 deleteMatchingCalendars(String selection, String[] selectionArgs)1275 private int deleteMatchingCalendars(String selection, String[] selectionArgs) { 1276 return mResolver.delete(mCalendarsUri, selection, selectionArgs); 1277 } 1278 insertEvent(int calId, EventInfo event)1279 private Uri insertEvent(int calId, EventInfo event) { 1280 return insertEvent(calId, event, null); 1281 } 1282 insertEvent(int calId, EventInfo event, ContentValues cv)1283 private Uri insertEvent(int calId, EventInfo event, ContentValues cv) { 1284 if (mWipe) { 1285 // Wipe instance table so it will be regenerated 1286 mMetaData.clearInstanceRange(); 1287 } 1288 1289 if (cv == null) { 1290 cv = eventInfoToContentValues(calId, event); 1291 } 1292 1293 Uri url = mResolver.insert(mEventsUri, cv); 1294 1295 // Create a fake _sync_id and add it to the event. Update the database 1296 // directly so that we don't trigger any validation checks in the 1297 // CalendarProvider. 1298 long id = ContentUris.parseId(url); 1299 mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id); 1300 event.mSyncId = mGlobalSyncId; 1301 mGlobalSyncId += 1; 1302 1303 return url; 1304 } 1305 eventInfoToContentValues(int calId, EventInfo event)1306 private ContentValues eventInfoToContentValues(int calId, EventInfo event) { 1307 ContentValues m = new ContentValues(); 1308 m.put(Events.CALENDAR_ID, calId); 1309 m.put(Events.TITLE, event.mTitle); 1310 m.put(Events.DTSTART, event.mDtstart); 1311 m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0); 1312 1313 if (event.mRrule == null || mForceDtend) { 1314 // This is a normal event 1315 m.put(Events.DTEND, event.mDtend); 1316 m.remove(Events.DURATION); 1317 } 1318 if (event.mRrule != null) { 1319 // This is a repeating event 1320 m.put(Events.RRULE, event.mRrule); 1321 m.put(Events.DURATION, event.mDuration); 1322 m.remove(Events.DTEND); 1323 } 1324 1325 if (event.mDescription != null) { 1326 m.put(Events.DESCRIPTION, event.mDescription); 1327 } 1328 if (event.mTimezone != null) { 1329 m.put(Events.EVENT_TIMEZONE, event.mTimezone); 1330 } 1331 if (event.mCustomAppPackage != null) { 1332 m.put(Events.CUSTOM_APP_PACKAGE, event.mCustomAppPackage); 1333 } 1334 if (event.mCustomAppUri != null) { 1335 m.put(Events.CUSTOM_APP_URI, event.mCustomAppUri); 1336 } 1337 if (event.mUid2445 != null) { 1338 m.put(Events.UID_2445, event.mUid2445); 1339 } 1340 1341 if (event.mOriginalTitle != null) { 1342 // This is a recurrence exception. 1343 EventInfo recur = findEvent(event.mOriginalTitle); 1344 assertNotNull(recur); 1345 String syncId = String.format("%d", recur.mSyncId); 1346 m.put(Events.ORIGINAL_SYNC_ID, syncId); 1347 m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0); 1348 m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance); 1349 } 1350 return m; 1351 } 1352 1353 /** 1354 * Deletes all the events that match the given title. 1355 * @param title the given title to match events on 1356 * @return the number of rows deleted 1357 */ deleteMatchingEvents(String title, String account, String accountType)1358 private int deleteMatchingEvents(String title, String account, String accountType) { 1359 Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID }, 1360 "title=?", new String[] { title }, null); 1361 int numRows = 0; 1362 while (cursor.moveToNext()) { 1363 long id = cursor.getLong(0); 1364 // Do delete as a sync adapter so event is really deleted, not just marked 1365 // as deleted. 1366 Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true, account, 1367 accountType); 1368 numRows += mResolver.delete(uri, null, null); 1369 } 1370 cursor.close(); 1371 return numRows; 1372 } 1373 1374 /** 1375 * Updates all the events that match the given title. 1376 * @param title the given title to match events on 1377 * @return the number of rows updated 1378 */ updateMatchingEvents(String title, ContentValues values)1379 private int updateMatchingEvents(String title, ContentValues values) { 1380 String[] projection = new String[] { 1381 Events._ID, 1382 Events.DTSTART, 1383 Events.DTEND, 1384 Events.DURATION, 1385 Events.ALL_DAY, 1386 Events.RRULE, 1387 Events.EVENT_TIMEZONE, 1388 Events.ORIGINAL_SYNC_ID, 1389 }; 1390 Cursor cursor = mResolver.query(mEventsUri, projection, 1391 "title=?", new String[] { title }, null); 1392 int numRows = 0; 1393 while (cursor.moveToNext()) { 1394 long id = cursor.getLong(0); 1395 1396 // If any of the following fields are being changed, then we need 1397 // to include all of them. 1398 if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND) 1399 || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY) 1400 || values.containsKey(Events.RRULE) 1401 || values.containsKey(Events.EVENT_TIMEZONE) 1402 || values.containsKey(CalendarContract.Events.STATUS)) { 1403 long dtstart = cursor.getLong(1); 1404 long dtend = cursor.getLong(2); 1405 String duration = cursor.getString(3); 1406 boolean allDay = cursor.getInt(4) != 0; 1407 String rrule = cursor.getString(5); 1408 String timezone = cursor.getString(6); 1409 String originalEvent = cursor.getString(7); 1410 1411 if (!values.containsKey(Events.DTSTART)) { 1412 values.put(Events.DTSTART, dtstart); 1413 } 1414 // Don't add DTEND for repeating events 1415 if (!values.containsKey(Events.DTEND) && rrule == null) { 1416 values.put(Events.DTEND, dtend); 1417 } 1418 if (!values.containsKey(Events.DURATION) && duration != null) { 1419 values.put(Events.DURATION, duration); 1420 } 1421 if (!values.containsKey(Events.ALL_DAY)) { 1422 values.put(Events.ALL_DAY, allDay ? 1 : 0); 1423 } 1424 if (!values.containsKey(Events.RRULE) && rrule != null) { 1425 values.put(Events.RRULE, rrule); 1426 } 1427 if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) { 1428 values.put(Events.EVENT_TIMEZONE, timezone); 1429 } 1430 if (!values.containsKey(Events.ORIGINAL_SYNC_ID) && originalEvent != null) { 1431 values.put(Events.ORIGINAL_SYNC_ID, originalEvent); 1432 } 1433 } 1434 1435 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 1436 numRows += mResolver.update(uri, values, null, null); 1437 } 1438 cursor.close(); 1439 return numRows; 1440 } 1441 1442 /** 1443 * Updates the status of all the events that match the given title. 1444 * @param title the given title to match events on 1445 * @return the number of rows updated 1446 */ updateMatchingEventsStatusOnly(String title, ContentValues values)1447 private int updateMatchingEventsStatusOnly(String title, ContentValues values) { 1448 String[] projection = new String[] { 1449 Events._ID, 1450 }; 1451 if (values.size() != 1 && !values.containsKey(Events.STATUS)) { 1452 return 0; 1453 } 1454 Cursor cursor = mResolver.query(mEventsUri, projection, 1455 "title=?", new String[] { title }, null); 1456 int numRows = 0; 1457 while (cursor.moveToNext()) { 1458 long id = cursor.getLong(0); 1459 1460 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 1461 numRows += mResolver.update(uri, values, null, null); 1462 } 1463 cursor.close(); 1464 return numRows; 1465 } 1466 1467 deleteAllEvents()1468 private void deleteAllEvents() { 1469 mDb.execSQL("DELETE FROM Events;"); 1470 mMetaData.clearInstanceRange(); 1471 } 1472 1473 /** 1474 * Creates an updated URI that includes query parameters that identify the source as a 1475 * sync adapter. 1476 */ asSyncAdapter(Uri uri, String account, String accountType)1477 static Uri asSyncAdapter(Uri uri, String account, String accountType) { 1478 return uri.buildUpon() 1479 .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, 1480 "true") 1481 .appendQueryParameter(Calendars.ACCOUNT_NAME, account) 1482 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); 1483 } 1484 testInsertUpdateDeleteColor()1485 public void testInsertUpdateDeleteColor() throws Exception { 1486 // Calendar Color 1487 long colorType = Colors.TYPE_CALENDAR; 1488 String colorKey = "123"; 1489 long colorValue = 11; 1490 long colorId = insertAndCheckColor(colorType, colorKey, colorValue); 1491 1492 try { 1493 insertAndCheckColor(colorType, colorKey, colorValue); 1494 fail("Expected to fail with duplicate insertion"); 1495 } catch (IllegalArgumentException iae) { 1496 // good 1497 } 1498 1499 // Test Update 1500 colorValue += 11; 1501 updateAndCheckColor(colorId, colorType, colorKey, colorValue); 1502 1503 // Event Color 1504 colorType = Colors.TYPE_EVENT; 1505 colorValue += 11; 1506 colorId = insertAndCheckColor(colorType, colorKey, colorValue); 1507 try { 1508 insertAndCheckColor(colorType, colorKey, colorValue); 1509 fail("Expected to fail with duplicate insertion"); 1510 } catch (IllegalArgumentException iae) { 1511 // good 1512 } 1513 1514 // Create an event with the old color value. 1515 int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); 1516 String title = "colorTest"; 1517 ContentValues cv = this.eventInfoToContentValues(calendarId0, mEvents[0]); 1518 cv.put(Events.EVENT_COLOR_KEY, colorKey); 1519 cv.put(Events.TITLE, title); 1520 Uri uri = insertEvent(calendarId0, mEvents[0], cv); 1521 Cursor c = mResolver.query(uri, new String[] {Events.EVENT_COLOR}, null, null, null); 1522 try { 1523 // Confirm the color is set. 1524 c.moveToFirst(); 1525 assertEquals(colorValue, c.getInt(0)); 1526 } finally { 1527 if (c != null) { 1528 c.close(); 1529 } 1530 } 1531 1532 // Test Update 1533 colorValue += 11; 1534 updateAndCheckColor(colorId, colorType, colorKey, colorValue); 1535 1536 // Check if color was updated in event. 1537 c = mResolver.query(uri, new String[] {Events.EVENT_COLOR}, null, null, null); 1538 try { 1539 c.moveToFirst(); 1540 assertEquals(colorValue, c.getInt(0)); 1541 } finally { 1542 if (c != null) { 1543 c.close(); 1544 } 1545 } 1546 1547 // Test Delete 1548 Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, DEFAULT_ACCOUNT, 1549 DEFAULT_ACCOUNT_TYPE); 1550 try { 1551 // Delete should fail if color referenced by an event. 1552 mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX, 1553 new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey}); 1554 fail("Should not allow deleting referenced color"); 1555 } catch (UnsupportedOperationException e) { 1556 // Exception expected. 1557 } 1558 Cursor cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY}, 1559 Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?", 1560 new String[] {colorKey, Long.toString(colorType)}, null); 1561 assertEquals(1, cursor.getCount()); 1562 1563 // Try again, by deleting the event, then the color. 1564 assertEquals(1, deleteMatchingEvents(title, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE)); 1565 mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX, 1566 new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey}); 1567 cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY}, 1568 Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?", 1569 new String[] {colorKey, Long.toString(colorType)}, null); 1570 assertEquals(0, cursor.getCount()); 1571 } 1572 checkColor(long colorId, long colorType, String colorKey, long color)1573 private void checkColor(long colorId, long colorType, String colorKey, long color) { 1574 String[] projection = new String[] { 1575 Colors.ACCOUNT_NAME, // 0 1576 Colors.ACCOUNT_TYPE, // 1 1577 Colors.COLOR_TYPE, // 2 1578 Colors.COLOR_KEY, // 3 1579 Colors.COLOR, // 4 1580 Colors._ID, // 5 1581 Colors.DATA, // 6 1582 }; 1583 Cursor cursor = mResolver.query(Colors.CONTENT_URI, projection, Colors.COLOR_KEY 1584 + "=? AND " + Colors.COLOR_TYPE + "=?", new String[] { 1585 colorKey, Long.toString(colorType) 1586 }, null /* sortOrder */); 1587 1588 assertEquals(1, cursor.getCount()); 1589 1590 assertTrue(cursor.moveToFirst()); 1591 assertEquals(DEFAULT_ACCOUNT, cursor.getString(0)); 1592 assertEquals(DEFAULT_ACCOUNT_TYPE, cursor.getString(1)); 1593 assertEquals(colorType, cursor.getLong(2)); 1594 assertEquals(colorKey, cursor.getString(3)); 1595 assertEquals(color, cursor.getLong(4)); 1596 assertEquals(colorId, cursor.getLong(5)); 1597 assertEquals(obsToString(colorType, colorKey, color), cursor.getString(6)); 1598 cursor.close(); 1599 } 1600 insertAndCheckColor(long colorType, String colorKey, long color)1601 private long insertAndCheckColor(long colorType, String colorKey, long color) { 1602 Uri uri = insertColor(colorType, colorKey, color); 1603 long id = Long.parseLong(uri.getLastPathSegment()); 1604 1605 checkColor(id, colorType, colorKey, color); 1606 return id; 1607 } 1608 testInsertNormalEvents()1609 public void testInsertNormalEvents() throws Exception { 1610 final int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1611 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 1612 assertEquals(0, cursor.getCount()); 1613 cursor.close(); 1614 1615 // Keep track of the number of normal events 1616 int numOfInserts = 0; 1617 1618 // "begin" is the earliest start time of all the normal events, 1619 // and "end" is the latest end time of all the normal events. 1620 long begin = 0, end = 0; 1621 1622 int len = mEvents.length; 1623 Uri[] uris = new Uri[len]; 1624 ContentValues[] cvs = new ContentValues[len]; 1625 for (int ii = 0; ii < len; ii++) { 1626 EventInfo event = mEvents[ii]; 1627 // Skip repeating events and recurrence exceptions 1628 if (event.mRrule != null || event.mOriginalTitle != null) { 1629 continue; 1630 } 1631 if (numOfInserts == 0) { 1632 begin = event.mDtstart; 1633 end = event.mDtend; 1634 } else { 1635 if (begin > event.mDtstart) { 1636 begin = event.mDtstart; 1637 } 1638 if (end < event.mDtend) { 1639 end = event.mDtend; 1640 } 1641 } 1642 1643 cvs[ii] = eventInfoToContentValues(calId, event); 1644 uris[ii] = insertEvent(calId, event, cvs[ii]); 1645 numOfInserts += 1; 1646 } 1647 1648 // Verify 1649 for (int i = 0; i < len; i++) { 1650 if (cvs[i] == null) continue; 1651 assertNotNull(uris[i]); 1652 cursor = mResolver.query(uris[i], null, null, null, null); 1653 assertEquals("Item " + i + " not found", 1, cursor.getCount()); 1654 verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor); 1655 cursor.close(); 1656 } 1657 1658 // query all 1659 cursor = mResolver.query(mEventsUri, null, null, null, null); 1660 assertEquals(numOfInserts, cursor.getCount()); 1661 cursor.close(); 1662 1663 // Check that the Instances table has one instance of each of the 1664 // normal events. 1665 cursor = queryInstances(begin, end); 1666 assertEquals(numOfInserts, cursor.getCount()); 1667 cursor.close(); 1668 } 1669 testInsertRepeatingEvents()1670 public void testInsertRepeatingEvents() throws Exception { 1671 Cursor cursor; 1672 Uri url = null; 1673 1674 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1675 1676 cursor = mResolver.query(mEventsUri, null, null, null, null); 1677 assertEquals(0, cursor.getCount()); 1678 cursor.close(); 1679 1680 // Keep track of the number of repeating events 1681 int numOfInserts = 0; 1682 1683 int len = mEvents.length; 1684 Uri[] uris = new Uri[len]; 1685 ContentValues[] cvs = new ContentValues[len]; 1686 for (int ii = 0; ii < len; ii++) { 1687 EventInfo event = mEvents[ii]; 1688 // Skip normal events 1689 if (event.mRrule == null) { 1690 continue; 1691 } 1692 cvs[ii] = eventInfoToContentValues(calId, event); 1693 uris[ii] = insertEvent(calId, event, cvs[ii]); 1694 numOfInserts += 1; 1695 } 1696 1697 // Verify 1698 for (int i = 0; i < len; i++) { 1699 if (cvs[i] == null) continue; 1700 assertNotNull(uris[i]); 1701 cursor = mResolver.query(uris[i], null, null, null, null); 1702 assertEquals("Item " + i + " not found", 1, cursor.getCount()); 1703 verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor); 1704 cursor.close(); 1705 } 1706 1707 // query all 1708 cursor = mResolver.query(mEventsUri, null, null, null, null); 1709 assertEquals(numOfInserts, cursor.getCount()); 1710 cursor.close(); 1711 } 1712 1713 // Force a dtend value to be set and make sure instance expansion still works testInstanceRangeDtend()1714 public void testInstanceRangeDtend() throws Exception { 1715 mForceDtend = true; 1716 testInstanceRange(); 1717 } 1718 testInstanceRange()1719 public void testInstanceRange() throws Exception { 1720 Cursor cursor; 1721 Uri url = null; 1722 1723 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1724 1725 cursor = mResolver.query(mEventsUri, null, null, null, null); 1726 assertEquals(0, cursor.getCount()); 1727 cursor.close(); 1728 1729 int len = mInstanceRanges.length; 1730 for (int ii = 0; ii < len; ii++) { 1731 InstanceInfo instance = mInstanceRanges[ii]; 1732 EventInfo event = instance.mEvent; 1733 url = insertEvent(calId, event); 1734 cursor = queryInstances(instance.mBegin, instance.mEnd); 1735 if (instance.mExpectedOccurrences != cursor.getCount()) { 1736 Log.e(TAG, "Test failed! Instance index: " + ii); 1737 Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription 1738 + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]" 1739 + " expected: " + instance.mExpectedOccurrences); 1740 dumpCursor(cursor); 1741 } 1742 assertEquals(instance.mExpectedOccurrences, cursor.getCount()); 1743 cursor.close(); 1744 // Delete as sync_adapter so event is really deleted. 1745 int rows = mResolver.delete( 1746 updatedUri(url, true, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 1747 null /* selection */, null /* selection args */); 1748 assertEquals(1, rows); 1749 } 1750 } 1751 assertArrayEquals(T[] expected, T[] actual)1752 public static <T> void assertArrayEquals(T[] expected, T[] actual) { 1753 if (!Arrays.equals(expected, actual)) { 1754 fail("expected:<" + Arrays.toString(expected) + 1755 "> but was:<" + Arrays.toString(actual) + ">"); 1756 } 1757 } 1758 1759 @SmallTest testEscapeSearchToken()1760 public void testEscapeSearchToken() { 1761 String token = "test"; 1762 String expected = "test"; 1763 assertEquals(expected, mProvider.escapeSearchToken(token)); 1764 1765 token = "%"; 1766 expected = "#%"; 1767 assertEquals(expected, mProvider.escapeSearchToken(token)); 1768 1769 token = "_"; 1770 expected = "#_"; 1771 assertEquals(expected, mProvider.escapeSearchToken(token)); 1772 1773 token = "#"; 1774 expected = "##"; 1775 assertEquals(expected, mProvider.escapeSearchToken(token)); 1776 1777 token = "##"; 1778 expected = "####"; 1779 assertEquals(expected, mProvider.escapeSearchToken(token)); 1780 1781 token = "%_#"; 1782 expected = "#%#_##"; 1783 assertEquals(expected, mProvider.escapeSearchToken(token)); 1784 1785 token = "blah%blah"; 1786 expected = "blah#%blah"; 1787 assertEquals(expected, mProvider.escapeSearchToken(token)); 1788 } 1789 1790 @SmallTest testTokenizeSearchQuery()1791 public void testTokenizeSearchQuery() { 1792 String query = ""; 1793 String[] expectedTokens = new String[] {}; 1794 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1795 1796 query = "a"; 1797 expectedTokens = new String[] {"a"}; 1798 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1799 1800 query = "word"; 1801 expectedTokens = new String[] {"word"}; 1802 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1803 1804 query = "two words"; 1805 expectedTokens = new String[] {"two", "words"}; 1806 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1807 1808 query = "test, punctuation."; 1809 expectedTokens = new String[] {"test", "punctuation"}; 1810 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1811 1812 query = "\"test phrase\""; 1813 expectedTokens = new String[] {"test phrase"}; 1814 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1815 1816 query = "unquoted \"this is quoted\""; 1817 expectedTokens = new String[] {"unquoted", "this is quoted"}; 1818 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1819 1820 query = " \"this is quoted\" unquoted "; 1821 expectedTokens = new String[] {"this is quoted", "unquoted"}; 1822 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1823 1824 query = "escap%e m_e"; 1825 expectedTokens = new String[] {"escap#%e", "m#_e"}; 1826 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1827 1828 query = "'a bunch' of malformed\" things"; 1829 expectedTokens = new String[] {"a", "bunch", "of", "malformed", "things"}; 1830 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1831 1832 query = "''''''....,.''trim punctuation"; 1833 expectedTokens = new String[] {"trim", "punctuation"}; 1834 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1835 } 1836 1837 @SmallTest testConstructSearchWhere()1838 public void testConstructSearchWhere() { 1839 String[] tokens = new String[] {"red"}; 1840 String expected = "(title LIKE ? ESCAPE \"#\" OR " 1841 + "description LIKE ? ESCAPE \"#\" OR " 1842 + "eventLocation LIKE ? ESCAPE \"#\" OR " 1843 + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " 1844 + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )"; 1845 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1846 1847 tokens = new String[] {}; 1848 expected = ""; 1849 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1850 1851 tokens = new String[] {"red", "green"}; 1852 expected = "(title LIKE ? ESCAPE \"#\" OR " 1853 + "description LIKE ? ESCAPE \"#\" OR " 1854 + "eventLocation LIKE ? ESCAPE \"#\" OR " 1855 + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " 1856 + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND " 1857 + "(title LIKE ? ESCAPE \"#\" OR " 1858 + "description LIKE ? ESCAPE \"#\" OR " 1859 + "eventLocation LIKE ? ESCAPE \"#\" OR " 1860 + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " 1861 + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )"; 1862 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1863 1864 tokens = new String[] {"red blue", "green"}; 1865 expected = "(title LIKE ? ESCAPE \"#\" OR " 1866 + "description LIKE ? ESCAPE \"#\" OR " 1867 + "eventLocation LIKE ? ESCAPE \"#\" OR " 1868 + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " 1869 + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND " 1870 + "(title LIKE ? ESCAPE \"#\" OR " 1871 + "description LIKE ? ESCAPE \"#\" OR " 1872 + "eventLocation LIKE ? ESCAPE \"#\" OR " 1873 + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " 1874 + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )"; 1875 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1876 } 1877 1878 @SmallTest testConstructSearchArgs()1879 public void testConstructSearchArgs() { 1880 String[] tokens = new String[] {"red"}; 1881 String[] expected = new String[] {"%red%", "%red%", 1882 "%red%", "%red%", "%red%" }; 1883 assertArrayEquals(expected, mProvider.constructSearchArgs(tokens)); 1884 1885 tokens = new String[] {"red", "blue"}; 1886 expected = new String[] { "%red%", "%red%", "%red%", 1887 "%red%", "%red%", "%blue%", "%blue%", 1888 "%blue%", "%blue%","%blue%"}; 1889 assertArrayEquals(expected, mProvider.constructSearchArgs(tokens)); 1890 1891 tokens = new String[] {}; 1892 expected = new String[] {}; 1893 assertArrayEquals(expected, mProvider.constructSearchArgs(tokens)); 1894 } 1895 testInstanceSearchQuery()1896 public void testInstanceSearchQuery() throws Exception { 1897 final String[] PROJECTION = new String[] { 1898 Instances.TITLE, // 0 1899 Instances.EVENT_LOCATION, // 1 1900 Instances.ALL_DAY, // 2 1901 Instances.CALENDAR_COLOR, // 3 1902 Instances.EVENT_TIMEZONE, // 4 1903 Instances.EVENT_ID, // 5 1904 Instances.BEGIN, // 6 1905 Instances.END, // 7 1906 Instances._ID, // 8 1907 Instances.START_DAY, // 9 1908 Instances.END_DAY, // 10 1909 Instances.START_MINUTE, // 11 1910 Instances.END_MINUTE, // 12 1911 Instances.HAS_ALARM, // 13 1912 Instances.RRULE, // 14 1913 Instances.RDATE, // 15 1914 Instances.SELF_ATTENDEE_STATUS, // 16 1915 Events.ORGANIZER, // 17 1916 Events.GUESTS_CAN_MODIFY, // 18 1917 }; 1918 1919 String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW; 1920 String where = Instances.SELF_ATTENDEE_STATUS + "!=" + 1921 CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED; 1922 1923 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1924 final String START = "2008-05-01T00:00:00"; 1925 final String END = "2008-05-01T20:00:00"; 1926 1927 EventInfo event1 = new EventInfo("search orange", 1928 START, 1929 END, 1930 false /* allDay */, 1931 DEFAULT_TIMEZONE); 1932 event1.mDescription = "this is description1"; 1933 1934 EventInfo event2 = new EventInfo("search purple", 1935 START, 1936 END, 1937 false /* allDay */, 1938 DEFAULT_TIMEZONE); 1939 event2.mDescription = "lasers, out of nowhere"; 1940 1941 EventInfo event3 = new EventInfo("", 1942 START, 1943 END, 1944 false /* allDay */, 1945 DEFAULT_TIMEZONE); 1946 event3.mDescription = "kapow"; 1947 1948 EventInfo[] events = { event1, event2, event3 }; 1949 1950 insertEvent(calId, events[0]); 1951 insertEvent(calId, events[1]); 1952 insertEvent(calId, events[2]); 1953 1954 Time time = new Time(DEFAULT_TIMEZONE); 1955 time.parse3339(START); 1956 long startMs = time.toMillis(); 1957 // Query starting from way in the past to one hour into the event. 1958 // Query is more than 2 months so the range won't get extended by the provider. 1959 Cursor cursor = null; 1960 1961 try { 1962 cursor = queryInstances(mResolver, PROJECTION, 1963 startMs - DateUtils.YEAR_IN_MILLIS, 1964 startMs + DateUtils.HOUR_IN_MILLIS, 1965 "search", where, null, orderBy); 1966 assertEquals(2, cursor.getCount()); 1967 } finally { 1968 if (cursor != null) { 1969 cursor.close(); 1970 } 1971 } 1972 1973 try { 1974 cursor = queryInstances(mResolver, PROJECTION, 1975 startMs - DateUtils.YEAR_IN_MILLIS, 1976 startMs + DateUtils.HOUR_IN_MILLIS, 1977 "purple", where, null, orderBy); 1978 assertEquals(1, cursor.getCount()); 1979 } finally { 1980 if (cursor != null) { 1981 cursor.close(); 1982 } 1983 } 1984 1985 try { 1986 cursor = queryInstances(mResolver, PROJECTION, 1987 startMs - DateUtils.YEAR_IN_MILLIS, 1988 startMs + DateUtils.HOUR_IN_MILLIS, 1989 "puurple", where, null, orderBy); 1990 assertEquals(0, cursor.getCount()); 1991 } finally { 1992 if (cursor != null) { 1993 cursor.close(); 1994 } 1995 } 1996 1997 try { 1998 cursor = queryInstances(mResolver, PROJECTION, 1999 startMs - DateUtils.YEAR_IN_MILLIS, 2000 startMs + DateUtils.HOUR_IN_MILLIS, 2001 "purple lasers", where, null, orderBy); 2002 assertEquals(1, cursor.getCount()); 2003 } finally { 2004 if (cursor != null) { 2005 cursor.close(); 2006 } 2007 } 2008 2009 try { 2010 cursor = queryInstances(mResolver, PROJECTION, 2011 startMs - DateUtils.YEAR_IN_MILLIS, 2012 startMs + DateUtils.HOUR_IN_MILLIS, 2013 "lasers kapow", where, null, orderBy); 2014 assertEquals(0, cursor.getCount()); 2015 } finally { 2016 if (cursor != null) { 2017 cursor.close(); 2018 } 2019 } 2020 2021 try { 2022 cursor = queryInstances(mResolver, PROJECTION, 2023 startMs - DateUtils.YEAR_IN_MILLIS, 2024 startMs + DateUtils.HOUR_IN_MILLIS, 2025 "\"search purple\"", where, null, orderBy); 2026 assertEquals(1, cursor.getCount()); 2027 } finally { 2028 if (cursor != null) { 2029 cursor.close(); 2030 } 2031 } 2032 2033 try { 2034 cursor = queryInstances(mResolver, PROJECTION, 2035 startMs - DateUtils.YEAR_IN_MILLIS, 2036 startMs + DateUtils.HOUR_IN_MILLIS, 2037 "\"purple search\"", where, null, orderBy); 2038 assertEquals(0, cursor.getCount()); 2039 } finally { 2040 if (cursor != null) { 2041 cursor.close(); 2042 } 2043 } 2044 2045 try { 2046 cursor = queryInstances(mResolver, PROJECTION, 2047 startMs - DateUtils.YEAR_IN_MILLIS, 2048 startMs + DateUtils.HOUR_IN_MILLIS, 2049 "%", where, null, orderBy); 2050 assertEquals(0, cursor.getCount()); 2051 } finally { 2052 if (cursor != null) { 2053 cursor.close(); 2054 } 2055 } 2056 } 2057 testDeleteCalendar()2058 public void testDeleteCalendar() throws Exception { 2059 int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); 2060 int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com"); 2061 insertEvent(calendarId0, mEvents[0]); 2062 insertEvent(calendarId1, mEvents[1]); 2063 // Should have 2 calendars and 2 events 2064 testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 2); 2065 testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 2); 2066 2067 int deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI, 2068 "ownerAccount='user2@google.com'", null /* selectionArgs */); 2069 2070 assertEquals(1, deletes); 2071 // Should have 1 calendar and 1 event 2072 testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 1); 2073 testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 1); 2074 2075 deletes = mResolver.delete(Uri.withAppendedPath(CalendarContract.Calendars.CONTENT_URI, 2076 String.valueOf(calendarId0)), 2077 null /* selection*/ , null /* selectionArgs */); 2078 2079 assertEquals(1, deletes); 2080 // Should have 0 calendars and 0 events 2081 testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 0); 2082 testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 0); 2083 2084 deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI, 2085 "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */); 2086 2087 assertEquals(0, deletes); 2088 } 2089 testCalendarAlerts()2090 public void testCalendarAlerts() throws Exception { 2091 // This projection is from AlertActivity; want to make sure it works. 2092 String[] projection = new String[] { 2093 CalendarContract.CalendarAlerts._ID, // 0 2094 CalendarContract.CalendarAlerts.TITLE, // 1 2095 CalendarContract.CalendarAlerts.EVENT_LOCATION, // 2 2096 CalendarContract.CalendarAlerts.ALL_DAY, // 3 2097 CalendarContract.CalendarAlerts.BEGIN, // 4 2098 CalendarContract.CalendarAlerts.END, // 5 2099 CalendarContract.CalendarAlerts.EVENT_ID, // 6 2100 CalendarContract.CalendarAlerts.CALENDAR_COLOR, // 7 2101 CalendarContract.CalendarAlerts.RRULE, // 8 2102 CalendarContract.CalendarAlerts.HAS_ALARM, // 9 2103 CalendarContract.CalendarAlerts.STATE, // 10 2104 CalendarContract.CalendarAlerts.ALARM_TIME, // 11 2105 }; 2106 2107 mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE); 2108 String calendarIdString = Integer.toString(mCalendarId); 2109 checkEvents(0, mDb, calendarIdString); 2110 Uri eventUri = insertEvent(mCalendarId, findEvent("normal0")); 2111 checkEvents(1, mDb, calendarIdString); 2112 long eventId = ContentUris.parseId(eventUri); 2113 2114 Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, eventId /* eventId */, 2115 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */); 2116 CalendarContract.CalendarAlerts.insert(mResolver, eventId /* eventId */, 2117 2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */); 2118 2119 // Regular query 2120 Cursor cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI, projection, 2121 null /* selection */, null /* selectionArgs */, null /* sortOrder */); 2122 2123 assertEquals(2, cursor.getCount()); 2124 cursor.close(); 2125 2126 // Instance query 2127 cursor = mResolver.query(alertUri, projection, 2128 null /* selection */, null /* selectionArgs */, null /* sortOrder */); 2129 2130 assertEquals(1, cursor.getCount()); 2131 cursor.close(); 2132 2133 // Grouped by event query 2134 cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI_BY_INSTANCE, 2135 projection, null /* selection */, null /* selectionArgs */, null /* sortOrder */); 2136 2137 assertEquals(1, cursor.getCount()); 2138 cursor.close(); 2139 } 2140 testInsertAlertToNonExistentEvent()2141 public void testInsertAlertToNonExistentEvent() { 2142 Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, 1 /* eventId */, 2143 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */); 2144 assertEquals(null, alertUri); 2145 } 2146 testInsertReminderToNonExistentEvent()2147 public void testInsertReminderToNonExistentEvent() { 2148 ContentValues reminder = new ContentValues(); 2149 reminder.put(CalendarContract.Reminders.MINUTES, 30); 2150 reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL); 2151 reminder.put(CalendarContract.Attendees.EVENT_ID, 1); 2152 Uri reminderUri = mResolver.insert( 2153 updatedUri(CalendarContract.Reminders.CONTENT_URI, true, DEFAULT_ACCOUNT, 2154 DEFAULT_ACCOUNT_TYPE), reminder); 2155 assertEquals(null, reminderUri); 2156 } 2157 testInsertAttendeeToNonExistentEvent()2158 public void testInsertAttendeeToNonExistentEvent() { 2159 ContentValues attendee = new ContentValues(); 2160 attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe"); 2161 attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT); 2162 attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE, 2163 CalendarContract.Attendees.TYPE_REQUIRED); 2164 attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, 2165 CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); 2166 attendee.put(CalendarContract.Attendees.EVENT_ID, 1); 2167 attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID1"); 2168 attendee.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, "IDNS1"); 2169 Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee); 2170 assertEquals(null, attendeesUri); 2171 } 2172 testInsertExtendedPropertyToNonExistentEvent()2173 public void testInsertExtendedPropertyToNonExistentEvent() { 2174 ContentValues extended = new ContentValues(); 2175 extended.put(CalendarContract.ExtendedProperties.NAME, "foo"); 2176 extended.put(CalendarContract.ExtendedProperties.VALUE, "bar"); 2177 extended.put(CalendarContract.ExtendedProperties.EVENT_ID, 1); 2178 2179 Uri extendedUri = mResolver.insert( 2180 updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, true, 2181 DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended); 2182 assertEquals(null, extendedUri); 2183 } 2184 checkEvents(int count, SQLiteDatabase db)2185 void checkEvents(int count, SQLiteDatabase db) { 2186 Cursor cursor = db.query("Events", null, null, null, null, null, null); 2187 try { 2188 assertEquals(count, cursor.getCount()); 2189 } finally { 2190 cursor.close(); 2191 } 2192 } 2193 checkEvents(int count, SQLiteDatabase db, String calendar)2194 void checkEvents(int count, SQLiteDatabase db, String calendar) { 2195 Cursor cursor = db.query("Events", null, Events.CALENDAR_ID + "=?", new String[] {calendar}, 2196 null, null, null); 2197 try { 2198 assertEquals(count, cursor.getCount()); 2199 } finally { 2200 cursor.close(); 2201 } 2202 } 2203 2204 2205 // TODO Reenable this when we are ready to work on this 2206 // 2207 // public void testToShowInsertIsSlowForRecurringEvents() throws Exception { 2208 // mCalendarId = insertCal("CalendarTestToShowInsertIsSlowForRecurringEvents", DEFAULT_TIMEZONE); 2209 // String calendarIdString = Integer.toString(mCalendarId); 2210 // long testStart = System.currentTimeMillis(); 2211 // 2212 // final int testTrials = 100; 2213 // 2214 // for (int i = 0; i < testTrials; i++) { 2215 // checkEvents(i, mDb, calendarIdString); 2216 // long insertStartTime = System.currentTimeMillis(); 2217 // Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 2218 // Log.e(TAG, i + ") insertion time " + (System.currentTimeMillis() - insertStartTime)); 2219 // } 2220 // Log.e(TAG, " Avg insertion time = " + (System.currentTimeMillis() - testStart)/testTrials); 2221 // } 2222 2223 /** 2224 * Test attendee processing 2225 * @throws Exception 2226 */ testAttendees()2227 public void testAttendees() throws Exception { 2228 mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE); 2229 String calendarIdString = Integer.toString(mCalendarId); 2230 checkEvents(0, mDb, calendarIdString); 2231 Uri eventUri = insertEvent(mCalendarId, findEvent("normal0")); 2232 checkEvents(1, mDb, calendarIdString); 2233 long eventId = ContentUris.parseId(eventUri); 2234 2235 ContentValues attendee = new ContentValues(); 2236 attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe"); 2237 attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT); 2238 attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE, 2239 CalendarContract.Attendees.TYPE_REQUIRED); 2240 attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, 2241 CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); 2242 attendee.put(CalendarContract.Attendees.EVENT_ID, eventId); 2243 attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID1"); 2244 attendee.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, "IDNS1"); 2245 Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee); 2246 2247 Cursor cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null, 2248 "event_id=" + eventId, null, null); 2249 assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1, 2250 cursor.getCount()); 2251 Set<String> attendeeColumns = attendee.keySet(); 2252 verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor); 2253 cursor.close(); 2254 2255 cursor = mResolver.query(eventUri, null, null, null, null); 2256 // TODO figure out why this test fails. App works fine for this case. 2257 assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1, 2258 cursor.getCount()); 2259 int selfColumn = cursor.getColumnIndex(CalendarContract.Events.SELF_ATTENDEE_STATUS); 2260 cursor.moveToNext(); 2261 long selfAttendeeStatus = cursor.getInt(selfColumn); 2262 assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus); 2263 cursor.close(); 2264 2265 // Update status to declined and change identity 2266 ContentValues attendeeUpdate = new ContentValues(); 2267 attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2"); 2268 attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2"); 2269 attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_STATUS, 2270 CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED); 2271 attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS, 2272 CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED); 2273 mResolver.update(attendeesUri, attendeeUpdate, null, null); 2274 2275 // Check in attendees table 2276 cursor = mResolver.query(attendeesUri, null, null, null, null); 2277 cursor.moveToNext(); 2278 verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor); 2279 cursor.close(); 2280 2281 // Test that the self status in events table is updated 2282 cursor = mResolver.query(eventUri, null, null, null, null); 2283 cursor.moveToNext(); 2284 selfAttendeeStatus = cursor.getInt(selfColumn); 2285 assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 2286 cursor.close(); 2287 2288 // Add another attendee 2289 attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Dude"); 2290 attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, "dude@dude.com"); 2291 attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS, 2292 CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED); 2293 mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee); 2294 2295 cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null, 2296 "event_id=" + eventId, null, null); 2297 assertEquals(2, cursor.getCount()); 2298 cursor.close(); 2299 2300 cursor = mResolver.query(eventUri, null, null, null, null); 2301 cursor.moveToNext(); 2302 selfAttendeeStatus = cursor.getInt(selfColumn); 2303 assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 2304 cursor.close(); 2305 } 2306 verifyContentValueAgainstCursor(ContentValues cv, Set<String> keys, Cursor cursor)2307 private void verifyContentValueAgainstCursor(ContentValues cv, 2308 Set<String> keys, Cursor cursor) { 2309 cursor.moveToFirst(); 2310 for (String key : keys) { 2311 assertEquals(cv.get(key).toString(), 2312 cursor.getString(cursor.getColumnIndex(key))); 2313 } 2314 cursor.close(); 2315 } 2316 2317 /** 2318 * Test the event's dirty status and clear it. 2319 * 2320 * @param eventId event to fetch. 2321 * @param wanted the wanted dirty status 2322 */ testAndClearDirty(long eventId, int wanted)2323 private void testAndClearDirty(long eventId, int wanted) { 2324 Cursor cursor = mResolver.query( 2325 ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId), 2326 null, null, null, null); 2327 try { 2328 assertEquals("Event count", 1, cursor.getCount()); 2329 cursor.moveToNext(); 2330 int dirty = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.DIRTY)); 2331 assertEquals("dirty flag", wanted, dirty); 2332 if (dirty == 1) { 2333 // Have to access database directly since provider will set dirty again. 2334 mDb.execSQL("UPDATE Events SET " + Events.DIRTY + "=0 WHERE _id=" + eventId); 2335 } 2336 } finally { 2337 cursor.close(); 2338 } 2339 } 2340 2341 /** 2342 * Test the count of results from a query. 2343 * @param uri The URI to query 2344 * @param where The where string or null. 2345 * @param wanted The number of results wanted. An assertion is thrown if it doesn't match. 2346 */ testQueryCount(Uri uri, String where, int wanted)2347 private void testQueryCount(Uri uri, String where, int wanted) { 2348 Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */, 2349 null /* sortOrder */); 2350 try { 2351 assertEquals("query results", wanted, cursor.getCount()); 2352 } finally { 2353 cursor.close(); 2354 } 2355 } 2356 2357 /** 2358 * Test dirty flag processing. 2359 * @throws Exception 2360 */ testDirty()2361 public void testDirty() throws Exception { 2362 internalTestDirty(false); 2363 } 2364 2365 /** 2366 * Test dirty flag processing for updates from a sync adapter. 2367 * @throws Exception 2368 */ testDirtyWithSyncAdapter()2369 public void testDirtyWithSyncAdapter() throws Exception { 2370 internalTestDirty(true); 2371 } 2372 2373 /** 2374 * Adds CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation. Otherwise, 2375 * returns the original URI. 2376 */ updatedUri(Uri uri, boolean syncAdapter, String account, String accountType)2377 private Uri updatedUri(Uri uri, boolean syncAdapter, String account, String accountType) { 2378 if (syncAdapter) { 2379 return addSyncQueryParams(uri, account, accountType); 2380 } else { 2381 return uri; 2382 } 2383 } 2384 2385 /** 2386 * Test dirty flag processing either for syncAdapter operations or client operations. 2387 * The main difference is syncAdapter operations don't set the dirty bit. 2388 */ internalTestDirty(boolean syncAdapter)2389 private void internalTestDirty(boolean syncAdapter) throws Exception { 2390 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2391 2392 long now = System.currentTimeMillis(); 2393 long begin = (now / 1000) * 1000; 2394 long end = begin + ONE_HOUR_MILLIS; 2395 Time time = new Time(DEFAULT_TIMEZONE); 2396 time.set(begin); 2397 String startDate = time.format3339(false); 2398 time.set(end); 2399 String endDate = time.format3339(false); 2400 2401 EventInfo eventInfo = new EventInfo("current", startDate, endDate, false); 2402 Uri eventUri = insertEvent(mCalendarId, eventInfo); 2403 2404 long eventId = ContentUris.parseId(eventUri); 2405 testAndClearDirty(eventId, 1); 2406 2407 ContentValues attendee = new ContentValues(); 2408 attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe"); 2409 attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT); 2410 attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE, 2411 CalendarContract.Attendees.TYPE_REQUIRED); 2412 attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, 2413 CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); 2414 attendee.put(CalendarContract.Attendees.EVENT_ID, eventId); 2415 2416 Uri attendeeUri = mResolver.insert( 2417 updatedUri(CalendarContract.Attendees.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT, 2418 DEFAULT_ACCOUNT_TYPE), 2419 attendee); 2420 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2421 testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1); 2422 2423 ContentValues reminder = new ContentValues(); 2424 reminder.put(CalendarContract.Reminders.MINUTES, 30); 2425 reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL); 2426 reminder.put(CalendarContract.Attendees.EVENT_ID, eventId); 2427 2428 Uri reminderUri = mResolver.insert( 2429 updatedUri(CalendarContract.Reminders.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT, 2430 DEFAULT_ACCOUNT_TYPE), reminder); 2431 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2432 testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 1); 2433 2434 long alarmTime = begin + 5 * ONE_MINUTE_MILLIS; 2435 2436 ContentValues alert = new ContentValues(); 2437 alert.put(CalendarContract.CalendarAlerts.BEGIN, begin); 2438 alert.put(CalendarContract.CalendarAlerts.END, end); 2439 alert.put(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime); 2440 alert.put(CalendarContract.CalendarAlerts.CREATION_TIME, now); 2441 alert.put(CalendarContract.CalendarAlerts.RECEIVED_TIME, now); 2442 alert.put(CalendarContract.CalendarAlerts.NOTIFY_TIME, now); 2443 alert.put(CalendarContract.CalendarAlerts.STATE, 2444 CalendarContract.CalendarAlerts.STATE_SCHEDULED); 2445 alert.put(CalendarContract.CalendarAlerts.MINUTES, 30); 2446 alert.put(CalendarContract.CalendarAlerts.EVENT_ID, eventId); 2447 2448 Uri alertUri = mResolver.insert( 2449 updatedUri(CalendarContract.CalendarAlerts.CONTENT_URI, syncAdapter, 2450 DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert); 2451 // Alerts don't dirty the event 2452 testAndClearDirty(eventId, 0); 2453 testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); 2454 2455 ContentValues extended = new ContentValues(); 2456 extended.put(CalendarContract.ExtendedProperties.NAME, "foo"); 2457 extended.put(CalendarContract.ExtendedProperties.VALUE, "bar"); 2458 extended.put(CalendarContract.ExtendedProperties.EVENT_ID, eventId); 2459 2460 Uri extendedUri = null; 2461 if (syncAdapter) { 2462 // Only the sync adapter is allowed to modify ExtendedProperties. 2463 extendedUri = mResolver.insert( 2464 updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter, 2465 DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended); 2466 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2467 testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, 2468 "event_id=" + eventId, 1); 2469 } else { 2470 // Confirm that inserting as app fails. 2471 try { 2472 extendedUri = mResolver.insert( 2473 updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter, 2474 DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended); 2475 fail("Only sync adapter should be allowed to insert into ExtendedProperties"); 2476 } catch (IllegalArgumentException iae) {} 2477 } 2478 2479 // Now test updates 2480 2481 attendee = new ContentValues(); 2482 attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Sam"); 2483 2484 assertEquals("update", 1, mResolver.update( 2485 updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 2486 attendee, 2487 null /* where */, null /* selectionArgs */)); 2488 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2489 2490 testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1); 2491 2492 alert = new ContentValues(); 2493 alert.put(CalendarContract.CalendarAlerts.STATE, 2494 CalendarContract.CalendarAlerts.STATE_DISMISSED); 2495 2496 assertEquals("update", 1, mResolver.update( 2497 updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert, 2498 null /* where */, null /* selectionArgs */)); 2499 // Alerts don't dirty the event 2500 testAndClearDirty(eventId, 0); 2501 testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); 2502 2503 extended = new ContentValues(); 2504 extended.put(CalendarContract.ExtendedProperties.VALUE, "baz"); 2505 2506 if (syncAdapter) { 2507 assertEquals("update", 1, mResolver.update( 2508 updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 2509 extended, 2510 null /* where */, null /* selectionArgs */)); 2511 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2512 testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, 2513 "event_id=" + eventId, 1); 2514 } 2515 2516 // Now test deletes 2517 2518 assertEquals("delete", 1, mResolver.delete( 2519 updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 2520 null, null /* selectionArgs */)); 2521 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2522 testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 0); 2523 2524 assertEquals("delete", 1, mResolver.delete( 2525 updatedUri(reminderUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 2526 null /* where */, null /* selectionArgs */)); 2527 2528 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2529 testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 0); 2530 2531 assertEquals("delete", 1, mResolver.delete( 2532 updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 2533 null /* where */, null /* selectionArgs */)); 2534 2535 // Alerts don't dirty the event 2536 testAndClearDirty(eventId, 0); 2537 testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0); 2538 2539 if (syncAdapter) { 2540 assertEquals("delete", 1, mResolver.delete( 2541 updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), 2542 null /* where */, null /* selectionArgs */)); 2543 2544 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 2545 testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 2546 0); 2547 } 2548 } 2549 2550 /** 2551 * Test calendar deletion 2552 * @throws Exception 2553 */ testCalendarDeletion()2554 public void testCalendarDeletion() throws Exception { 2555 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2556 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 2557 long eventId = ContentUris.parseId(eventUri); 2558 testAndClearDirty(eventId, 1); 2559 Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1")); 2560 long eventId1 = ContentUris.parseId(eventUri); 2561 assertEquals("delete", 1, mResolver.delete(eventUri1, null, null)); 2562 // Calendar has one event and one deleted event 2563 testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2); 2564 2565 assertEquals("delete", 1, mResolver.delete(CalendarContract.Calendars.CONTENT_URI, 2566 "_id=" + mCalendarId, null)); 2567 // Calendar should be deleted 2568 testQueryCount(CalendarContract.Calendars.CONTENT_URI, null, 0); 2569 // Event should be gone 2570 testQueryCount(CalendarContract.Events.CONTENT_URI, null, 0); 2571 } 2572 2573 /** 2574 * Test multiple account support. 2575 */ testMultipleAccounts()2576 public void testMultipleAccounts() throws Exception { 2577 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2578 int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com"); 2579 Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0")); 2580 Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1")); 2581 2582 testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2); 2583 Uri eventsWithAccount = CalendarContract.Events.CONTENT_URI.buildUpon() 2584 .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_NAME, DEFAULT_ACCOUNT) 2585 .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_TYPE, 2586 DEFAULT_ACCOUNT_TYPE) 2587 .build(); 2588 // Only one event for that account 2589 testQueryCount(eventsWithAccount, null, 1); 2590 2591 // Test deletion with account and selection 2592 2593 long eventId = ContentUris.parseId(eventUri1); 2594 // Wrong account, should not be deleted 2595 assertEquals("delete", 0, mResolver.delete( 2596 updatedUri(eventsWithAccount, true /* syncAdapter */, DEFAULT_ACCOUNT, 2597 DEFAULT_ACCOUNT_TYPE), 2598 "_id=" + eventId, null /* selectionArgs */)); 2599 testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2); 2600 // Right account, should be deleted 2601 assertEquals("delete", 1, mResolver.delete( 2602 updatedUri(CalendarContract.Events.CONTENT_URI, true /* syncAdapter */, 2603 "user2@google.com", DEFAULT_ACCOUNT_TYPE), 2604 "_id=" + eventId, null /* selectionArgs */)); 2605 testQueryCount(CalendarContract.Events.CONTENT_URI, null, 1); 2606 } 2607 2608 /** 2609 * Run commands, wiping instance table at each step. 2610 * This tests full instance expansion. 2611 * @throws Exception 2612 */ testCommandSequences1()2613 public void testCommandSequences1() throws Exception { 2614 commandSequences(true); 2615 } 2616 2617 /** 2618 * Run commands normally. 2619 * This tests incremental instance expansion. 2620 * @throws Exception 2621 */ testCommandSequences2()2622 public void testCommandSequences2() throws Exception { 2623 commandSequences(false); 2624 } 2625 2626 /** 2627 * Run thorough set of command sequences 2628 * @param wipe true if instances should be wiped and regenerated 2629 * @throws Exception 2630 */ commandSequences(boolean wipe)2631 private void commandSequences(boolean wipe) throws Exception { 2632 Cursor cursor; 2633 Uri url = null; 2634 mWipe = wipe; // Set global flag 2635 2636 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2637 2638 cursor = mResolver.query(mEventsUri, null, null, null, null); 2639 assertEquals(0, cursor.getCount()); 2640 cursor.close(); 2641 Command[] commands; 2642 2643 Log.i(TAG, "Normal insert/delete"); 2644 commands = mNormalInsertDelete; 2645 for (Command command : commands) { 2646 command.execute(); 2647 } 2648 2649 deleteAllEvents(); 2650 2651 Log.i(TAG, "All-day insert/delete"); 2652 commands = mAlldayInsertDelete; 2653 for (Command command : commands) { 2654 command.execute(); 2655 } 2656 2657 deleteAllEvents(); 2658 2659 Log.i(TAG, "Recurring insert/delete"); 2660 commands = mRecurringInsertDelete; 2661 for (Command command : commands) { 2662 command.execute(); 2663 } 2664 2665 deleteAllEvents(); 2666 2667 Log.i(TAG, "Exception with truncated recurrence"); 2668 commands = mExceptionWithTruncatedRecurrence; 2669 for (Command command : commands) { 2670 command.execute(); 2671 } 2672 2673 deleteAllEvents(); 2674 2675 Log.i(TAG, "Exception with moved recurrence"); 2676 commands = mExceptionWithMovedRecurrence; 2677 for (Command command : commands) { 2678 command.execute(); 2679 } 2680 2681 deleteAllEvents(); 2682 2683 Log.i(TAG, "Exception with cancel"); 2684 commands = mCancelInstance; 2685 for (Command command : commands) { 2686 command.execute(); 2687 } 2688 2689 deleteAllEvents(); 2690 2691 Log.i(TAG, "Exception with moved recurrence2"); 2692 commands = mExceptionWithMovedRecurrence2; 2693 for (Command command : commands) { 2694 command.execute(); 2695 } 2696 2697 deleteAllEvents(); 2698 2699 Log.i(TAG, "Exception with no recurrence"); 2700 commands = mExceptionWithNoRecurrence; 2701 for (Command command : commands) { 2702 command.execute(); 2703 } 2704 } 2705 2706 /** 2707 * Test Time toString. 2708 * @throws Exception 2709 */ 2710 // Suppressed because toString currently hangs. 2711 @Suppress testTimeToString()2712 public void testTimeToString() throws Exception { 2713 Time time = new Time(Time.TIMEZONE_UTC); 2714 String str = "2039-01-01T23:00:00.000Z"; 2715 String result = "20390101T230000UTC(0,0,0,-1,0)"; 2716 time.parse3339(str); 2717 assertEquals(result, time.toString()); 2718 } 2719 2720 /** 2721 * Test the query done by Event.loadEvents 2722 * Also test that instance queries work when an event straddles the expansion range 2723 * @throws Exception 2724 */ testInstanceQuery()2725 public void testInstanceQuery() throws Exception { 2726 final String[] PROJECTION = new String[] { 2727 Instances.TITLE, // 0 2728 Instances.EVENT_LOCATION, // 1 2729 Instances.ALL_DAY, // 2 2730 Instances.CALENDAR_COLOR, // 3 2731 Instances.EVENT_TIMEZONE, // 4 2732 Instances.EVENT_ID, // 5 2733 Instances.BEGIN, // 6 2734 Instances.END, // 7 2735 Instances._ID, // 8 2736 Instances.START_DAY, // 9 2737 Instances.END_DAY, // 10 2738 Instances.START_MINUTE, // 11 2739 Instances.END_MINUTE, // 12 2740 Instances.HAS_ALARM, // 13 2741 Instances.RRULE, // 14 2742 Instances.RDATE, // 15 2743 Instances.SELF_ATTENDEE_STATUS, // 16 2744 Events.ORGANIZER, // 17 2745 Events.GUESTS_CAN_MODIFY, // 18 2746 }; 2747 2748 String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW; 2749 String where = Instances.SELF_ATTENDEE_STATUS + "!=" 2750 + CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED; 2751 2752 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2753 final String START = "2008-05-01T00:00:00"; 2754 final String END = "2008-05-01T20:00:00"; 2755 2756 EventInfo[] events = { new EventInfo("normal0", 2757 START, 2758 END, 2759 false /* allDay */, 2760 DEFAULT_TIMEZONE) }; 2761 2762 insertEvent(calId, events[0]); 2763 2764 Time time = new Time(DEFAULT_TIMEZONE); 2765 time.parse3339(START); 2766 long startMs = time.toMillis(); 2767 // Query starting from way in the past to one hour into the event. 2768 // Query is more than 2 months so the range won't get extended by the provider. 2769 Cursor cursor = queryInstances(mResolver, PROJECTION, 2770 startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS, 2771 where, null, orderBy); 2772 try { 2773 assertEquals(1, cursor.getCount()); 2774 } finally { 2775 cursor.close(); 2776 } 2777 2778 // Now expand the instance range. The event overlaps the new part of the range. 2779 cursor = queryInstances(mResolver, PROJECTION, 2780 startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS, 2781 where, null, orderBy); 2782 try { 2783 assertEquals(1, cursor.getCount()); 2784 } finally { 2785 cursor.close(); 2786 } 2787 } 2788 2789 /** 2790 * Performs a query to return all visible instances in the given range that 2791 * match the given selection. This is a blocking function and should not be 2792 * done on the UI thread. This will cause an expansion of recurring events 2793 * to fill this time range if they are not already expanded and will slow 2794 * down for larger time ranges with many recurring events. 2795 * 2796 * @param cr The ContentResolver to use for the query 2797 * @param projection The columns to return 2798 * @param begin The start of the time range to query in UTC millis since 2799 * epoch 2800 * @param end The end of the time range to query in UTC millis since epoch 2801 * @param selection Filter on the query as an SQL WHERE statement 2802 * @param selectionArgs Args to replace any '?'s in the selection 2803 * @param orderBy How to order the rows as an SQL ORDER BY statement 2804 * @return A Cursor of instances matching the selection 2805 */ queryInstances(ContentResolver cr, String[] projection, long begin, long end, String selection, String[] selectionArgs, String orderBy)2806 private static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin, 2807 long end, String selection, String[] selectionArgs, String orderBy) { 2808 2809 Uri.Builder builder = Instances.CONTENT_URI.buildUpon(); 2810 ContentUris.appendId(builder, begin); 2811 ContentUris.appendId(builder, end); 2812 if (TextUtils.isEmpty(selection)) { 2813 selection = WHERE_CALENDARS_SELECTED; 2814 selectionArgs = WHERE_CALENDARS_ARGS; 2815 } else { 2816 selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED; 2817 if (selectionArgs != null && selectionArgs.length > 0) { 2818 selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1); 2819 selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0]; 2820 } else { 2821 selectionArgs = WHERE_CALENDARS_ARGS; 2822 } 2823 } 2824 return cr.query(builder.build(), projection, selection, selectionArgs, 2825 orderBy == null ? DEFAULT_SORT_ORDER : orderBy); 2826 } 2827 2828 /** 2829 * Performs a query to return all visible instances in the given range that 2830 * match the given selection. This is a blocking function and should not be 2831 * done on the UI thread. This will cause an expansion of recurring events 2832 * to fill this time range if they are not already expanded and will slow 2833 * down for larger time ranges with many recurring events. 2834 * 2835 * @param cr The ContentResolver to use for the query 2836 * @param projection The columns to return 2837 * @param begin The start of the time range to query in UTC millis since 2838 * epoch 2839 * @param end The end of the time range to query in UTC millis since epoch 2840 * @param searchQuery A string of space separated search terms. Segments 2841 * enclosed by double quotes will be treated as a single term. 2842 * @param selection Filter on the query as an SQL WHERE statement 2843 * @param selectionArgs Args to replace any '?'s in the selection 2844 * @param orderBy How to order the rows as an SQL ORDER BY statement 2845 * @return A Cursor of instances matching the selection 2846 */ queryInstances(ContentResolver cr, String[] projection, long begin, long end, String searchQuery, String selection, String[] selectionArgs, String orderBy)2847 public static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin, 2848 long end, String searchQuery, String selection, String[] selectionArgs, String orderBy) 2849 { 2850 Uri.Builder builder = Instances.CONTENT_SEARCH_URI.buildUpon(); 2851 ContentUris.appendId(builder, begin); 2852 ContentUris.appendId(builder, end); 2853 builder = builder.appendPath(searchQuery); 2854 if (TextUtils.isEmpty(selection)) { 2855 selection = WHERE_CALENDARS_SELECTED; 2856 selectionArgs = WHERE_CALENDARS_ARGS; 2857 } else { 2858 selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED; 2859 if (selectionArgs != null && selectionArgs.length > 0) { 2860 selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1); 2861 selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0]; 2862 } else { 2863 selectionArgs = WHERE_CALENDARS_ARGS; 2864 } 2865 } 2866 return cr.query(builder.build(), projection, selection, selectionArgs, 2867 orderBy == null ? DEFAULT_SORT_ORDER : orderBy); 2868 } 2869 queryInstances(long begin, long end)2870 private Cursor queryInstances(long begin, long end) { 2871 Uri url = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI, begin + "/" + end); 2872 return mResolver.query(url, null, null, null, null); 2873 } 2874 2875 protected static class MockProvider extends ContentProvider { 2876 2877 private String mAuthority; 2878 2879 private int mNumItems = 0; 2880 MockProvider(String authority)2881 public MockProvider(String authority) { 2882 mAuthority = authority; 2883 } 2884 2885 @Override onCreate()2886 public boolean onCreate() { 2887 return true; 2888 } 2889 2890 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)2891 public Cursor query(Uri uri, String[] projection, String selection, 2892 String[] selectionArgs, String sortOrder) { 2893 return new MatrixCursor(new String[]{ "_id" }, 0); 2894 } 2895 2896 @Override getType(Uri uri)2897 public String getType(Uri uri) { 2898 throw new UnsupportedOperationException(); 2899 } 2900 2901 @Override insert(Uri uri, ContentValues values)2902 public Uri insert(Uri uri, ContentValues values) { 2903 mNumItems++; 2904 return Uri.parse("content://" + mAuthority + "/" + mNumItems); 2905 } 2906 2907 @Override delete(Uri uri, String selection, String[] selectionArgs)2908 public int delete(Uri uri, String selection, String[] selectionArgs) { 2909 return 0; 2910 } 2911 2912 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)2913 public int update(Uri uri, ContentValues values, String selection, 2914 String[] selectionArgs) { 2915 return 0; 2916 } 2917 } 2918 cleanCalendarDataTable(SQLiteOpenHelper helper)2919 private void cleanCalendarDataTable(SQLiteOpenHelper helper) { 2920 if (null == helper) { 2921 return; 2922 } 2923 SQLiteDatabase db = helper.getWritableDatabase(); 2924 db.execSQL("DELETE FROM CalendarCache;"); 2925 } 2926 testGetAndSetTimezoneDatabaseVersion()2927 public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException { 2928 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 2929 cleanCalendarDataTable(helper); 2930 CalendarCache cache = new CalendarCache(helper); 2931 2932 boolean hasException = false; 2933 try { 2934 String value = cache.readData(null); 2935 } catch (CalendarCache.CacheException e) { 2936 hasException = true; 2937 } 2938 assertTrue(hasException); 2939 2940 assertNull(cache.readTimezoneDatabaseVersion()); 2941 2942 cache.writeTimezoneDatabaseVersion("1234"); 2943 assertEquals("1234", cache.readTimezoneDatabaseVersion()); 2944 2945 cache.writeTimezoneDatabaseVersion("5678"); 2946 assertEquals("5678", cache.readTimezoneDatabaseVersion()); 2947 } 2948 checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay)2949 private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) { 2950 Uri uri = Uri.parse("content://" + CalendarContract.AUTHORITY + "/events"); 2951 Log.i(TAG, "Looking for EventId = " + eventId); 2952 2953 Cursor cursor = mResolver.query(uri, null, null, null, null); 2954 assertEquals(1, cursor.getCount()); 2955 2956 int colIndexTitle = cursor.getColumnIndex(CalendarContract.Events.TITLE); 2957 int colIndexDtStart = cursor.getColumnIndex(CalendarContract.Events.DTSTART); 2958 int colIndexDtEnd = cursor.getColumnIndex(CalendarContract.Events.DTEND); 2959 int colIndexAllDay = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY); 2960 if (!cursor.moveToNext()) { 2961 Log.e(TAG,"Could not find inserted event"); 2962 assertTrue(false); 2963 } 2964 assertEquals(title, cursor.getString(colIndexTitle)); 2965 assertEquals(dtStart, cursor.getLong(colIndexDtStart)); 2966 assertEquals(dtEnd, cursor.getLong(colIndexDtEnd)); 2967 assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0)); 2968 cursor.close(); 2969 } 2970 testChangeTimezoneDB()2971 public void testChangeTimezoneDB() { 2972 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2973 2974 Cursor cursor = mResolver 2975 .query(CalendarContract.Events.CONTENT_URI, null, null, null, null); 2976 assertEquals(0, cursor.getCount()); 2977 cursor.close(); 2978 2979 EventInfo[] events = { new EventInfo("normal0", 2980 "2008-05-01T00:00:00", 2981 "2008-05-02T00:00:00", 2982 false, 2983 DEFAULT_TIMEZONE) }; 2984 2985 Uri uri = insertEvent(calId, events[0]); 2986 assertNotNull(uri); 2987 2988 // check the inserted event 2989 checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay); 2990 2991 // inject a new time zone 2992 getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE, 2993 MOCK_TIME_ZONE_DATABASE_VERSION); 2994 2995 // check timezone database version 2996 assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion()); 2997 2998 // check that the inserted event has *not* been updated 2999 checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay); 3000 } 3001 3002 public static final Uri PROPERTIES_CONTENT_URI = 3003 Uri.parse("content://" + CalendarContract.AUTHORITY + "/properties"); 3004 testGetProviderProperties()3005 public void testGetProviderProperties() throws CalendarCache.CacheException { 3006 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 3007 cleanCalendarDataTable(helper); 3008 CalendarCache cache = new CalendarCache(helper); 3009 3010 cache.writeTimezoneDatabaseVersion("2010k"); 3011 cache.writeTimezoneInstances("America/Denver"); 3012 cache.writeTimezoneInstancesPrevious("America/Los_Angeles"); 3013 cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO); 3014 3015 Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, null, null, null); 3016 assertEquals(4, cursor.getCount()); 3017 3018 final int keyColumnIndex = cursor.getColumnIndex(CalendarCache.COLUMN_NAME_KEY); 3019 final int valueColumnIndex = cursor.getColumnIndex(CalendarCache.COLUMN_NAME_VALUE); 3020 Map<String, String> map = new HashMap<String, String>(); 3021 3022 while (cursor.moveToNext()) { 3023 String key = cursor.getString(keyColumnIndex); 3024 String value = cursor.getString(valueColumnIndex); 3025 map.put(key, value); 3026 } 3027 3028 assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION)); 3029 assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_TYPE)); 3030 assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES)); 3031 assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)); 3032 3033 assertEquals("2010k", map.get(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION)); 3034 assertEquals("America/Denver", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES)); 3035 assertEquals("America/Los_Angeles", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)); 3036 assertEquals(CalendarCache.TIMEZONE_TYPE_AUTO, map.get(CalendarCache.KEY_TIMEZONE_TYPE)); 3037 3038 cursor.close(); 3039 } 3040 testGetProviderPropertiesByKey()3041 public void testGetProviderPropertiesByKey() throws CalendarCache.CacheException { 3042 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 3043 cleanCalendarDataTable(helper); 3044 CalendarCache cache = new CalendarCache(helper); 3045 3046 cache.writeTimezoneDatabaseVersion("2010k"); 3047 cache.writeTimezoneInstances("America/Denver"); 3048 cache.writeTimezoneInstancesPrevious("America/Los_Angeles"); 3049 cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO); 3050 3051 checkValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE); 3052 checkValueForKey("2010k", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION); 3053 checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES); 3054 checkValueForKey("America/Los_Angeles", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); 3055 } 3056 checkValueForKey(String value, String key)3057 private void checkValueForKey(String value, String key) { 3058 Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, 3059 "key=?", new String[] {key}, null); 3060 3061 assertEquals(1, cursor.getCount()); 3062 assertTrue(cursor.moveToFirst()); 3063 assertEquals(cursor.getString(cursor.getColumnIndex(CalendarCache.COLUMN_NAME_KEY)), 3064 key); 3065 assertEquals(cursor.getString(cursor.getColumnIndex(CalendarCache.COLUMN_NAME_VALUE)), 3066 value); 3067 3068 cursor.close(); 3069 } 3070 testUpdateProviderProperties()3071 public void testUpdateProviderProperties() throws CalendarCache.CacheException { 3072 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 3073 cleanCalendarDataTable(helper); 3074 CalendarCache cache = new CalendarCache(helper); 3075 3076 String localTimezone = TimeZone.getDefault().getID(); 3077 3078 // Set initial value 3079 cache.writeTimezoneDatabaseVersion("2010k"); 3080 3081 updateValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION); 3082 checkValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION); 3083 3084 // Set initial values 3085 cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO); 3086 cache.writeTimezoneInstances("America/Chicago"); 3087 cache.writeTimezoneInstancesPrevious("America/Denver"); 3088 3089 updateValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE); 3090 checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES); 3091 checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); 3092 3093 updateValueForKey(CalendarCache.TIMEZONE_TYPE_HOME, CalendarCache.KEY_TIMEZONE_TYPE); 3094 checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES); 3095 checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); 3096 3097 // Set initial value 3098 cache.writeTimezoneInstancesPrevious(""); 3099 updateValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES); 3100 checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES); 3101 checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); 3102 } 3103 updateValueForKey(String value, String key)3104 private void updateValueForKey(String value, String key) { 3105 ContentValues contentValues = new ContentValues(); 3106 contentValues.put(CalendarCache.COLUMN_NAME_VALUE, value); 3107 3108 int result = mResolver.update(PROPERTIES_CONTENT_URI, 3109 contentValues, 3110 CalendarCache.COLUMN_NAME_KEY + "=?", 3111 new String[] {key}); 3112 3113 assertEquals(1, result); 3114 } 3115 3116 /** 3117 * Verifies that the number of defined calendars meets expectations. 3118 * 3119 * @param expectedCount The number of calendars we expect to find. 3120 */ checkCalendarCount(int expectedCount)3121 private void checkCalendarCount(int expectedCount) { 3122 Cursor cursor = mResolver.query(mCalendarsUri, 3123 null /* projection */, 3124 null /* selection */, 3125 null /* selectionArgs */, 3126 null /* sortOrder */); 3127 assertEquals(expectedCount, cursor.getCount()); 3128 cursor.close(); 3129 } 3130 checkCalendarExists(int calId)3131 private void checkCalendarExists(int calId) { 3132 assertTrue(isCalendarExists(calId)); 3133 } 3134 checkCalendarDoesNotExists(int calId)3135 private void checkCalendarDoesNotExists(int calId) { 3136 assertFalse(isCalendarExists(calId)); 3137 } 3138 isCalendarExists(int calId)3139 private boolean isCalendarExists(int calId) { 3140 Cursor cursor = mResolver.query(mCalendarsUri, 3141 new String[] {Calendars._ID}, 3142 null /* selection */, 3143 null /* selectionArgs */, 3144 null /* sortOrder */); 3145 boolean found = false; 3146 while (cursor.moveToNext()) { 3147 if (calId == cursor.getInt(0)) { 3148 found = true; 3149 break; 3150 } 3151 } 3152 cursor.close(); 3153 return found; 3154 } 3155 testDeleteAllCalendars()3156 public void testDeleteAllCalendars() { 3157 checkCalendarCount(0); 3158 3159 insertCal("Calendar1", "America/Los_Angeles"); 3160 insertCal("Calendar2", "America/Los_Angeles"); 3161 3162 checkCalendarCount(2); 3163 3164 deleteMatchingCalendars(null /* selection */, null /* selectionArgs*/); 3165 checkCalendarCount(0); 3166 } 3167 testDeleteCalendarsWithSelection()3168 public void testDeleteCalendarsWithSelection() { 3169 checkCalendarCount(0); 3170 3171 int calId1 = insertCal("Calendar1", "America/Los_Angeles"); 3172 int calId2 = insertCal("Calendar2", "America/Los_Angeles"); 3173 3174 checkCalendarCount(2); 3175 checkCalendarExists(calId1); 3176 checkCalendarExists(calId2); 3177 3178 deleteMatchingCalendars(Calendars._ID + "=" + calId2, null /* selectionArgs*/); 3179 checkCalendarCount(1); 3180 checkCalendarExists(calId1); 3181 checkCalendarDoesNotExists(calId2); 3182 } 3183 testDeleteCalendarsWithSelectionAndArgs()3184 public void testDeleteCalendarsWithSelectionAndArgs() { 3185 checkCalendarCount(0); 3186 3187 int calId1 = insertCal("Calendar1", "America/Los_Angeles"); 3188 int calId2 = insertCal("Calendar2", "America/Los_Angeles"); 3189 3190 checkCalendarCount(2); 3191 checkCalendarExists(calId1); 3192 checkCalendarExists(calId2); 3193 3194 deleteMatchingCalendars(Calendars._ID + "=?", 3195 new String[] { Integer.toString(calId2) }); 3196 checkCalendarCount(1); 3197 checkCalendarExists(calId1); 3198 checkCalendarDoesNotExists(calId2); 3199 3200 deleteMatchingCalendars(Calendars._ID + "=?" + " AND " + Calendars.NAME + "=?", 3201 new String[] { Integer.toString(calId1), "Calendar1" }); 3202 checkCalendarCount(0); 3203 } 3204 testGetColumnIndex_IsPrimary()3205 public void testGetColumnIndex_IsPrimary() { 3206 checkCalendarCount(0); 3207 int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); 3208 3209 String[] projection = new String[] { 3210 Calendars.ACCOUNT_NAME, 3211 Calendars.CALENDAR_DISPLAY_NAME, 3212 Calendars.OWNER_ACCOUNT, 3213 Calendars.IS_PRIMARY 3214 }; 3215 String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))"; 3216 String[] selectionArgs = new String[] { 3217 DEFAULT_ACCOUNT 3218 }; 3219 Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, 3220 null); 3221 assertNotNull(cursor); 3222 assertEquals(3, cursor.getColumnIndex(Calendars.IS_PRIMARY)); 3223 cursor.close(); 3224 deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); 3225 checkCalendarCount(0); 3226 } 3227 testGetIsPrimary_ForEvents()3228 public void testGetIsPrimary_ForEvents() { 3229 checkCalendarCount(0); 3230 int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); 3231 3232 final String START = "2008-05-01T00:00:00"; 3233 final String END = "2008-05-01T20:00:00"; 3234 EventInfo event = new EventInfo("search orange", 3235 START, 3236 END, 3237 false /* allDay */, 3238 DEFAULT_TIMEZONE); 3239 3240 insertEvent(calendarId0, event); 3241 3242 String[] projection = new String[] { 3243 Calendars.IS_PRIMARY 3244 }; 3245 String selection = 3246 "((" + Calendars.IS_PRIMARY + " = ? OR " + Calendars.ACCOUNT_NAME + " = ?))"; 3247 String[] selectionArgs = new String[] { 3248 "1", DEFAULT_ACCOUNT 3249 }; 3250 Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, 3251 null); 3252 assertNotNull(cursor); 3253 cursor.moveToLast(); 3254 assertEquals(1, cursor.getCount()); 3255 assertEquals(1, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY))); 3256 cursor.close(); 3257 deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); 3258 checkCalendarCount(0); 3259 } 3260 testGetIsNotPrimary_ForEvents()3261 public void testGetIsNotPrimary_ForEvents() { 3262 checkCalendarCount(0); 3263 int calendarId0 = insertNonPrimaryCal("Calendar0", DEFAULT_TIMEZONE, DEFAULT_ACCOUNT); 3264 3265 final String START = "2008-05-01T00:00:00"; 3266 final String END = "2008-05-01T20:00:00"; 3267 EventInfo event = new EventInfo("search orange", 3268 START, 3269 END, 3270 false /* allDay */, 3271 DEFAULT_TIMEZONE); 3272 3273 insertEvent(calendarId0, event); 3274 3275 String[] projection = new String[] { 3276 Calendars.IS_PRIMARY 3277 }; 3278 String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))"; 3279 String[] selectionArgs = new String[] { 3280 DEFAULT_ACCOUNT 3281 }; 3282 Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, 3283 null); 3284 assertNotNull(cursor); 3285 cursor.moveToLast(); 3286 assertEquals(1, cursor.getCount()); 3287 assertEquals(0, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY))); 3288 cursor.close(); 3289 deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); 3290 checkCalendarCount(0); 3291 } 3292 testGetColumnIndex_Count()3293 public void testGetColumnIndex_Count() { 3294 checkCalendarCount(0); 3295 int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); 3296 3297 String[] projection = new String[] { 3298 BaseColumns._COUNT 3299 }; 3300 String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))"; 3301 String[] selectionArgs = new String[] { 3302 DEFAULT_ACCOUNT 3303 }; 3304 Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, 3305 null); 3306 assertNotNull(cursor); 3307 assertEquals(0, cursor.getColumnIndex(BaseColumns._COUNT)); 3308 cursor.close(); 3309 deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); 3310 checkCalendarCount(0); 3311 } 3312 testEnterpriseInstancesGetCorrectValue()3313 public void testEnterpriseInstancesGetCorrectValue() { 3314 final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); 3315 insertWorkEvent(WORK_EVENT_TITLE, calendarId, 3316 WORK_EVENT_DTSTART, WORK_EVENT_DTEND); 3317 insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, 3318 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); 3319 // Assume cross profile uri access is allowed by policy and settings. 3320 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3321 3322 Uri.Builder builder = Instances.ENTERPRISE_CONTENT_URI.buildUpon(); 3323 ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS); 3324 ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS); 3325 String[] projection = new String[]{ 3326 Instances.TITLE, 3327 Instances.CALENDAR_ID, 3328 Instances.DTSTART, 3329 Instances.CALENDAR_COLOR, 3330 }; 3331 Cursor cursor = mResolver.query( 3332 builder.build(), 3333 projection, null, null, null); 3334 3335 // Test the return cursor is correct when the all checks are met. 3336 assertNotNull(cursor); 3337 assertEquals(1, cursor.getCount()); 3338 cursor.moveToFirst(); 3339 assertEquals(WORK_EVENT_TITLE, cursor.getString(0)); 3340 assertEquals(calendarId, cursor.getLong(1)); 3341 assertEquals(WORK_EVENT_DTSTART, cursor.getLong(2)); 3342 assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(3)); 3343 cursor.close(); 3344 3345 cleanupEnterpriseTestForEvents(calendarId, 2); 3346 cleanupEnterpriseTestForCalendars(1); 3347 } 3348 testEnterpriseInstancesContentSearch()3349 public void testEnterpriseInstancesContentSearch() { 3350 final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); 3351 insertWorkEvent(WORK_EVENT_TITLE, calendarId, 3352 WORK_EVENT_DTSTART, WORK_EVENT_DTEND); 3353 insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, 3354 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); 3355 // Assume cross profile uri access is allowed by policy and settings. 3356 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3357 3358 Uri.Builder builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon(); 3359 ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS); 3360 ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS); 3361 builder = builder.appendPath(WORK_EVENT_TITLE /* search query */); 3362 Cursor cursor = mResolver.query( 3363 builder.build(), 3364 null, null, null, null); 3365 // There is only one event that meets the search criteria. 3366 assertNotNull(cursor); 3367 assertEquals(1, cursor.getCount()); 3368 3369 builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon(); 3370 ContentUris.appendId(builder, WORK_EVENT_DTSTART_STANDBY - DateUtils.YEAR_IN_MILLIS); 3371 ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS); 3372 builder = builder.appendPath(WORK_EVENT_DESCRIPTION /* search query */); 3373 cursor = mResolver.query( 3374 builder.build(), 3375 null, null, null, null); 3376 // There are two events that meet the search criteria. 3377 assertNotNull(cursor); 3378 assertEquals(2, cursor.getCount()); 3379 cursor.close(); 3380 3381 cleanupEnterpriseTestForEvents(calendarId, 2); 3382 cleanupEnterpriseTestForCalendars(1); 3383 } 3384 testEnterpriseEventsGetCorrectValue()3385 public void testEnterpriseEventsGetCorrectValue() { 3386 final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); 3387 final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId, 3388 WORK_EVENT_DTSTART, WORK_EVENT_DTEND); 3389 insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, 3390 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); 3391 // Assume cross profile uri access is allowed by policy and settings. 3392 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3393 3394 String selection = "(" + Events.TITLE + " = ? )"; 3395 String[] selectionArgs = new String[]{ 3396 WORK_EVENT_TITLE 3397 }; 3398 String[] projection = new String[]{ 3399 Events._ID, 3400 Events.TITLE, 3401 Events.CALENDAR_ID, 3402 Events.DTSTART, 3403 Calendars.CALENDAR_COLOR 3404 }; 3405 Cursor cursor = mResolver.query( 3406 Events.ENTERPRISE_CONTENT_URI, 3407 projection, selection, selectionArgs, null); 3408 3409 // Test the return cursor is correct when the all checks are met. 3410 assertNotNull(cursor); 3411 assertEquals(1, cursor.getCount()); 3412 cursor.moveToFirst(); 3413 assertEquals(idToTest, cursor.getLong(0)); 3414 assertEquals(WORK_EVENT_TITLE, cursor.getString(1)); 3415 assertEquals(calendarId, cursor.getLong(2)); 3416 assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3)); 3417 assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(4)); 3418 cursor.close(); 3419 3420 cleanupEnterpriseTestForEvents(calendarId, 2); 3421 cleanupEnterpriseTestForCalendars(1); 3422 } 3423 testEnterpriseEventsGetCorrectValueWithId()3424 public void testEnterpriseEventsGetCorrectValueWithId() { 3425 final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); 3426 final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId, 3427 WORK_EVENT_DTSTART, WORK_EVENT_DTEND); 3428 insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, 3429 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); 3430 // Assume cross profile uri access is allowed by policy and settings. 3431 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3432 3433 // Test ENTERPRISE_CONTENT_URI_ID. 3434 String[] projection = new String[]{ 3435 Events._ID, 3436 Events.TITLE, 3437 Events.CALENDAR_ID, 3438 Events.DTSTART, 3439 Calendars.CALENDAR_COLOR 3440 }; 3441 final Cursor cursor = mResolver.query( 3442 ContentUris.withAppendedId(Events.ENTERPRISE_CONTENT_URI, idToTest), 3443 projection, null, null, null); 3444 3445 assertNotNull(cursor); 3446 assertEquals(1, cursor.getCount()); 3447 cursor.moveToFirst(); 3448 assertEquals(idToTest, cursor.getLong(0)); 3449 assertEquals(WORK_EVENT_TITLE, cursor.getString(1)); 3450 assertEquals(calendarId, cursor.getLong(2)); 3451 assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3)); 3452 assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(4)); 3453 assertEquals(1, cursor.getInt(2)); 3454 cursor.close(); 3455 3456 cleanupEnterpriseTestForEvents(calendarId, 2); 3457 cleanupEnterpriseTestForCalendars(1); 3458 } 3459 testEnterpriseEventsProjectionCalibration()3460 public void testEnterpriseEventsProjectionCalibration() { 3461 final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); 3462 final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId, 3463 WORK_EVENT_DTSTART, WORK_EVENT_DTEND); 3464 insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, 3465 WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); 3466 // Assume cross profile uri access is allowed by policy and settings. 3467 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3468 3469 // Test all allowed list of columns are returned when projection is empty. 3470 String selection = "(" + Events.TITLE + " = ? )"; 3471 String[] selectionArgs = new String[]{ 3472 WORK_EVENT_TITLE 3473 }; 3474 final Cursor cursor = mResolver.query( 3475 Events.ENTERPRISE_CONTENT_URI, 3476 new String[] {}, selection, selectionArgs, null); 3477 3478 assertNotNull(cursor); 3479 assertEquals(1, cursor.getCount()); 3480 cursor.moveToFirst(); 3481 for (String column : CrossProfileCalendarHelper.EVENTS_TABLE_ALLOWED_LIST) { 3482 final int index = cursor.getColumnIndex(column); 3483 assertTrue(index != -1); 3484 } 3485 assertEquals(idToTest, cursor.getLong( 3486 cursor.getColumnIndex(Events._ID))); 3487 assertEquals(calendarId, cursor.getLong( 3488 cursor.getColumnIndex(Events.CALENDAR_ID))); 3489 assertEquals(WORK_CALENDAR_COLOR, cursor.getInt( 3490 cursor.getColumnIndex(Calendars.CALENDAR_COLOR))); 3491 assertEquals(1, cursor.getInt( 3492 cursor.getColumnIndex(Calendars.IS_PRIMARY))); 3493 cursor.close(); 3494 3495 cleanupEnterpriseTestForEvents(calendarId, 2); 3496 cleanupEnterpriseTestForCalendars(1); 3497 } 3498 cleanupEnterpriseTestForEvents(long calendarId, int numToDelete)3499 private void cleanupEnterpriseTestForEvents(long calendarId, int numToDelete) { 3500 // Selection arguments must be provided when deleting events. 3501 final int numDeleted = mWorkProfileProvider.delete(Events.CONTENT_URI, 3502 "(" + Events.CALENDAR_ID + " = ? )", 3503 new String[]{String.valueOf(calendarId)}); 3504 assertTrue(numDeleted == numToDelete); 3505 } 3506 insertWorkEvent(String eventTitle, long calendarId, long dtStart, long dtEnd)3507 private long insertWorkEvent(String eventTitle, long calendarId, long dtStart, long dtEnd) { 3508 final ContentValues cv = new ContentValues(); 3509 cv.put(Events.TITLE, eventTitle); 3510 cv.put(Events.CALENDAR_ID, calendarId); 3511 cv.put(Events.DESCRIPTION, WORK_EVENT_DESCRIPTION); 3512 cv.put(Events.EVENT_LOCATION, WORK_EVENT_LOCATION); 3513 cv.put(Events.EVENT_COLOR, WORK_EVENT_COLOR); 3514 cv.put(Events.DTSTART, dtStart); 3515 cv.put(Events.DTEND, dtEnd); 3516 cv.put(Events.EVENT_TIMEZONE, WORK_DEFAULT_TIMEZONE); 3517 final Uri uri = mWorkProfileProvider.insert(Events.CONTENT_URI, cv); 3518 return Long.parseLong(uri.getLastPathSegment()); 3519 } 3520 testEnterpriseCalendarGetCorrectValue()3521 public void testEnterpriseCalendarGetCorrectValue() { 3522 final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE); 3523 insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY); 3524 // Assume cross profile uri access is allowed by policy and settings. 3525 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3526 3527 // Test the return cursor is correct when the all checks are met. 3528 String selection = "(" + Calendars.CALENDAR_DISPLAY_NAME + " = ? )"; 3529 String[] selectionArgs = new String[] { 3530 WORK_CALENDAR_TITLE 3531 }; 3532 String[] projection = new String[] { 3533 Calendars._ID, 3534 Calendars.CALENDAR_COLOR 3535 }; 3536 Cursor cursor = mResolver.query( 3537 Calendars.ENTERPRISE_CONTENT_URI, 3538 projection, selection, selectionArgs, null); 3539 3540 assertNotNull(cursor); 3541 assertEquals(1, cursor.getCount()); 3542 cursor.moveToFirst(); 3543 assertEquals(idToTest, cursor.getLong(0)); 3544 assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(1)); 3545 3546 cleanupEnterpriseTestForCalendars(2); 3547 } 3548 testEnterpriseCalendarGetCorrectValueWithId()3549 public void testEnterpriseCalendarGetCorrectValueWithId() { 3550 final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE); 3551 insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY); 3552 // Assume cross profile uri access is allowed by policy and settings. 3553 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3554 3555 // Test Calendars.ENTERPRISE_CONTENT_URI with id. 3556 String[] projection = new String[] { 3557 Calendars._ID, 3558 Calendars.CALENDAR_COLOR 3559 }; 3560 final Cursor cursor = mResolver.query( 3561 ContentUris.withAppendedId(Calendars.ENTERPRISE_CONTENT_URI, idToTest), 3562 projection, null, null, null); 3563 3564 assertNotNull(cursor); 3565 assertEquals(1, cursor.getCount()); 3566 cursor.moveToFirst(); 3567 assertEquals(idToTest, cursor.getLong(0)); 3568 assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(1)); 3569 cursor.close(); 3570 3571 cleanupEnterpriseTestForCalendars(2); 3572 } 3573 testEnterpriseCalendarsProjectionCalibration()3574 public void testEnterpriseCalendarsProjectionCalibration() { 3575 final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE); 3576 // Assume cross profile uri access is allowed by policy and settings. 3577 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); 3578 3579 // Test all allowed list of columns are returned when projection is empty. 3580 final Cursor cursor = mResolver.query( 3581 Calendars.ENTERPRISE_CONTENT_URI, 3582 new String[] {}, null, null, null); 3583 3584 assertNotNull(cursor); 3585 assertEquals(1, cursor.getCount()); 3586 cursor.moveToFirst(); 3587 for (String column : CrossProfileCalendarHelper.CALENDARS_TABLE_ALLOWED_LIST) { 3588 final int index = cursor.getColumnIndex(column); 3589 assertTrue(index != -1); 3590 } 3591 assertEquals(idToTest, cursor.getLong( 3592 cursor.getColumnIndex(Calendars._ID))); 3593 assertEquals(WORK_CALENDAR_COLOR, cursor.getInt( 3594 cursor.getColumnIndex(Calendars.CALENDAR_COLOR))); 3595 cursor.close(); 3596 3597 cleanupEnterpriseTestForCalendars(1); 3598 } 3599 testEnterpriseCalendarsNonAllowedListProjection()3600 public void testEnterpriseCalendarsNonAllowedListProjection() { 3601 // Test SecurityException is thrown there is non-allowed list column in the projection. 3602 try { 3603 String[] projection = new String[] { 3604 Calendars._ID, 3605 Calendars.CALENDAR_DISPLAY_NAME, 3606 Calendars.CALENDAR_COLOR, 3607 Calendars.OWNER_ACCOUNT 3608 }; 3609 mResolver.query( 3610 Calendars.ENTERPRISE_CONTENT_URI, 3611 projection, null, null, null); 3612 fail("IllegalArgumentException is not thrown when querying non-allowed list of columns"); 3613 } catch (IllegalArgumentException e) { 3614 } 3615 } 3616 testEnterpriseCalendarsNotAllowed()3617 public void testEnterpriseCalendarsNotAllowed() { 3618 insertWorkCalendar(WORK_CALENDAR_TITLE); 3619 // Assume cross profile uri access is not allowed by policy or disabled in settings. 3620 MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(false); 3621 3622 // Throw exception if cross profile calendar is disabled in settings. 3623 try { 3624 final Cursor cursor = mResolver.query( 3625 Calendars.ENTERPRISE_CONTENT_URI, 3626 new String[]{}, null, null, null); 3627 fail("Unsupported operation exception should have been raised."); 3628 } catch (UnsupportedOperationException e) { 3629 // Exception expected. 3630 } 3631 cleanupEnterpriseTestForCalendars(1); 3632 } 3633 3634 // Remove the two inserted calendars. cleanupEnterpriseTestForCalendars(int numToDelete)3635 private void cleanupEnterpriseTestForCalendars(int numToDelete) { 3636 final int numDeleted = mWorkProfileProvider.delete(Calendars.CONTENT_URI, null, null); 3637 assertTrue(numDeleted == numToDelete); 3638 } 3639 insertWorkCalendar(String displayName)3640 private long insertWorkCalendar(String displayName) { 3641 final ContentValues cv = new ContentValues(); 3642 cv.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); 3643 cv.put(Calendars.OWNER_ACCOUNT, DEFAULT_ACCOUNT); 3644 cv.put(Calendars.ACCOUNT_NAME, DEFAULT_ACCOUNT); 3645 cv.put(Calendars.CALENDAR_DISPLAY_NAME, displayName); 3646 cv.put(Calendars.CALENDAR_COLOR, WORK_CALENDAR_COLOR); 3647 cv.put(Calendars.CALENDAR_TIME_ZONE, WORK_DEFAULT_TIMEZONE); 3648 final Uri uri = mWorkProfileProvider.insert( 3649 addSyncQueryParams(Calendars.CONTENT_URI, "local_account", 3650 CalendarContract.ACCOUNT_TYPE_LOCAL), cv); 3651 return Long.parseLong(uri.getLastPathSegment()); 3652 } 3653 } 3654