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