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