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