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