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