CalendarProvider2Test.java revision 81d904d66bd746c077cc0baa6cf1f51fe030eac4
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.providers.calendar; 18 19import android.content.ComponentName; 20import android.content.ContentProvider; 21import android.content.ContentUris; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.Entity; 25import android.content.EntityIterator; 26import android.content.Intent; 27import android.content.res.Resources; 28import android.database.Cursor; 29import android.database.sqlite.SQLiteDatabase; 30import android.database.sqlite.SQLiteOpenHelper; 31import android.net.Uri; 32import android.provider.Calendar; 33import android.provider.Calendar.Calendars; 34import android.provider.Calendar.Events; 35import android.provider.Calendar.EventsEntity; 36import android.provider.Calendar.Instances; 37import android.test.AndroidTestCase; 38import android.test.IsolatedContext; 39import android.test.RenamingDelegatingContext; 40import android.test.mock.MockContentResolver; 41import android.test.mock.MockContext; 42import android.test.suitebuilder.annotation.SmallTest; 43import android.test.suitebuilder.annotation.Smoke; 44import android.test.suitebuilder.annotation.Suppress; 45import android.text.format.DateUtils; 46import android.text.format.Time; 47import android.util.Log; 48 49import com.android.common.ArrayListCursor; 50import com.android.providers.calendar.CalendarProvider2Test.MockProvider; 51 52import java.io.File; 53import java.util.ArrayList; 54import java.util.Arrays; 55 56/** 57 * Runs various tests on an isolated Calendar provider with its own database. 58 * 59 * You can run the tests with the following command line: 60 * 61 * adb shell am instrument 62 * -e debug false 63 * -w 64 * -e class com.android.providers.calendar.CalendarProvider2Test 65 * com.android.providers.calendar.tests/android.test.InstrumentationTestRunner 66 * 67 * This test no longer extends ProviderTestCase2 because it actually doesn't 68 * allow you to inject a custom context (which we needed to mock out the calls 69 * to start a service). We the next best thing, which is copy the relevant code 70 * from PTC2 and extend AndroidTestCase instead. 71 */ 72// flaky test, add back to LargeTest when fixed - bug 2395696 73// @LargeTest 74public class CalendarProvider2Test extends AndroidTestCase { 75 static final String TAG = "calendar"; 76 77 private CalendarProvider2ForTesting mProvider; 78 private SQLiteDatabase mDb; 79 private MetaData mMetaData; 80 private Context mContext; 81 private MockContentResolver mResolver; 82 private Uri mEventsUri = Events.CONTENT_URI; 83 private int mCalendarId; 84 85 protected boolean mWipe = false; 86 protected boolean mForceDtend = false; 87 88 // We need a unique id to put in the _sync_id field so that we can create 89 // recurrence exceptions that refer to recurring events. 90 private int mGlobalSyncId = 1000; 91 private static final String CALENDAR_URL = 92 "http://www.google.com/calendar/feeds/joe%40joe.com/private/full"; 93 94 private static final String TIME_ZONE_AMERICA_ANCHORAGE = "America/Anchorage"; 95 private static final String TIME_ZONE_AMERICA_LOS_ANGELES = "America/Los_Angeles"; 96 private static final String DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES; 97 98 private static final String MOCK_TIME_ZONE_DATABASE_VERSION = "2010a"; 99 100 101 /** 102 * We need a few more stub methods so that our tests can run 103 */ 104 protected class MockContext2 extends MockContext { 105 106 @Override 107 public String getPackageName() { 108 return getContext().getPackageName(); 109 } 110 111 @Override 112 public Resources getResources() { 113 return getContext().getResources(); 114 } 115 116 @Override 117 public File getDir(String name, int mode) { 118 // name the directory so the directory will be seperated from 119 // one created through the regular Context 120 return getContext().getDir("mockcontext2_" + name, mode); 121 } 122 123 @Override 124 public ComponentName startService(Intent service) { 125 return null; 126 } 127 128 @Override 129 public boolean stopService(Intent service) { 130 return false; 131 } 132 } 133 134 /** 135 * KeyValue is a simple class that stores a pair of strings representing 136 * a (key, value) pair. This is used for updating events. 137 */ 138 private class KeyValue { 139 String key; 140 String value; 141 142 public KeyValue(String key, String value) { 143 this.key = key; 144 this.value = value; 145 } 146 } 147 148 /** 149 * A generic command interface. This is used to support a sequence of 150 * commands that can create events, delete or update events, and then 151 * check that the state of the database is as expected. 152 */ 153 private interface Command { 154 public void execute(); 155 } 156 157 /** 158 * This is used to insert a new event into the database. The event is 159 * specified by its name (or "title"). All of the event fields (the 160 * start and end time, whether it is an all-day event, and so on) are 161 * stored in a separate table (the "mEvents" table). 162 */ 163 private class Insert implements Command { 164 EventInfo eventInfo; 165 166 public Insert(String eventName) { 167 eventInfo = findEvent(eventName); 168 } 169 170 public void execute() { 171 Log.i(TAG, "insert " + eventInfo.mTitle); 172 insertEvent(mCalendarId, eventInfo); 173 } 174 } 175 176 /** 177 * This is used to delete an event, specified by the event name. 178 */ 179 private class Delete implements Command { 180 String eventName; 181 int expected; 182 183 public Delete(String eventName, int expected) { 184 this.eventName = eventName; 185 this.expected = expected; 186 } 187 188 public void execute() { 189 Log.i(TAG, "delete " + eventName); 190 int rows = deleteMatchingEvents(eventName); 191 assertEquals(expected, rows); 192 } 193 } 194 195 /** 196 * This is used to update an event. The values to update are specified 197 * with an array of (key, value) pairs. Both the key and value are 198 * specified as strings. Event fields that are not really strings (such 199 * as DTSTART which is a long) should be converted to the appropriate type 200 * but that isn't supported yet. When needed, that can be added here 201 * by checking for specific keys and converting the associated values. 202 */ 203 private class Update implements Command { 204 String eventName; 205 KeyValue[] pairs; 206 207 public Update(String eventName, KeyValue[] pairs) { 208 this.eventName = eventName; 209 this.pairs = pairs; 210 } 211 212 public void execute() { 213 Log.i(TAG, "update " + eventName); 214 if (mWipe) { 215 // Wipe instance table so it will be regenerated 216 mMetaData.clearInstanceRange(); 217 } 218 ContentValues map = new ContentValues(); 219 for (KeyValue pair : pairs) { 220 String value = pair.value; 221 if (Calendar.EventsColumns.STATUS.equals(pair.key)) { 222 // Do type conversion for STATUS 223 map.put(pair.key, Integer.parseInt(value)); 224 } else { 225 map.put(pair.key, value); 226 } 227 } 228 updateMatchingEvents(eventName, map); 229 } 230 } 231 232 /** 233 * This command queries the number of events and compares it to the given 234 * expected value. 235 */ 236 private class QueryNumEvents implements Command { 237 int expected; 238 239 public QueryNumEvents(int expected) { 240 this.expected = expected; 241 } 242 243 public void execute() { 244 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 245 assertEquals(expected, cursor.getCount()); 246 cursor.close(); 247 } 248 } 249 250 251 /** 252 * This command dumps the list of events to the log for debugging. 253 */ 254 private class DumpEvents implements Command { 255 256 public DumpEvents() { 257 } 258 259 public void execute() { 260 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 261 dumpCursor(cursor); 262 cursor.close(); 263 } 264 } 265 266 /** 267 * This command dumps the list of instances to the log for debugging. 268 */ 269 private class DumpInstances implements Command { 270 long begin; 271 long end; 272 273 public DumpInstances(String startDate, String endDate) { 274 Time time = new Time(DEFAULT_TIMEZONE); 275 time.parse3339(startDate); 276 begin = time.toMillis(false /* use isDst */); 277 time.parse3339(endDate); 278 end = time.toMillis(false /* use isDst */); 279 } 280 281 public void execute() { 282 Cursor cursor = queryInstances(begin, end); 283 dumpCursor(cursor); 284 cursor.close(); 285 } 286 } 287 288 /** 289 * This command queries the number of instances and compares it to the given 290 * expected value. 291 */ 292 private class QueryNumInstances implements Command { 293 int expected; 294 long begin; 295 long end; 296 297 public QueryNumInstances(String startDate, String endDate, int expected) { 298 Time time = new Time(DEFAULT_TIMEZONE); 299 time.parse3339(startDate); 300 begin = time.toMillis(false /* use isDst */); 301 time.parse3339(endDate); 302 end = time.toMillis(false /* use isDst */); 303 this.expected = expected; 304 } 305 306 public void execute() { 307 Cursor cursor = queryInstances(begin, end); 308 assertEquals(expected, cursor.getCount()); 309 cursor.close(); 310 } 311 } 312 313 /** 314 * When this command runs it verifies that all of the instances in the 315 * given range match the expected instances (each instance is specified by 316 * a start date). 317 * If you just want to verify that an instance exists in a given date 318 * range, use {@link VerifyInstance} instead. 319 */ 320 private class VerifyAllInstances implements Command { 321 long[] instances; 322 long begin; 323 long end; 324 325 public VerifyAllInstances(String startDate, String endDate, String[] dates) { 326 Time time = new Time(DEFAULT_TIMEZONE); 327 time.parse3339(startDate); 328 begin = time.toMillis(false /* use isDst */); 329 time.parse3339(endDate); 330 end = time.toMillis(false /* use isDst */); 331 332 if (dates == null) { 333 return; 334 } 335 336 // Convert all the instance date strings to UTC milliseconds 337 int len = dates.length; 338 this.instances = new long[len]; 339 int index = 0; 340 for (String instance : dates) { 341 time.parse3339(instance); 342 this.instances[index++] = time.toMillis(false /* use isDst */); 343 } 344 } 345 346 public void execute() { 347 Cursor cursor = queryInstances(begin, end); 348 int len = 0; 349 if (instances != null) { 350 len = instances.length; 351 } 352 if (len != cursor.getCount()) { 353 dumpCursor(cursor); 354 } 355 assertEquals("number of instances don't match", len, cursor.getCount()); 356 357 if (instances == null) { 358 return; 359 } 360 361 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 362 while (cursor.moveToNext()) { 363 long begin = cursor.getLong(beginColumn); 364 365 // Search the list of expected instances for a matching start 366 // time. 367 boolean found = false; 368 for (long instance : instances) { 369 if (instance == begin) { 370 found = true; 371 break; 372 } 373 } 374 if (!found) { 375 int titleColumn = cursor.getColumnIndex(Events.TITLE); 376 int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY); 377 378 String title = cursor.getString(titleColumn); 379 boolean allDay = cursor.getInt(allDayColumn) != 0; 380 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | 381 DateUtils.FORMAT_24HOUR; 382 if (allDay) { 383 flags |= DateUtils.FORMAT_UTC; 384 } else { 385 flags |= DateUtils.FORMAT_SHOW_TIME; 386 } 387 String date = DateUtils.formatDateRange(mContext, begin, begin, flags); 388 String mesg = String.format("Test failed!" 389 + " unexpected instance (\"%s\") at %s", 390 title, date); 391 Log.e(TAG, mesg); 392 } 393 if (!found) { 394 dumpCursor(cursor); 395 } 396 assertTrue(found); 397 } 398 cursor.close(); 399 } 400 } 401 402 /** 403 * When this command runs it verifies that the given instance exists in 404 * the given date range. 405 */ 406 private class VerifyInstance implements Command { 407 long instance; 408 boolean allDay; 409 long begin; 410 long end; 411 412 /** 413 * Creates a command to check that the given range [startDate,endDate] 414 * contains a specific instance of an event (specified by "date"). 415 * 416 * @param startDate the beginning of the date range 417 * @param endDate the end of the date range 418 * @param date the date or date-time string of an event instance 419 */ 420 public VerifyInstance(String startDate, String endDate, String date) { 421 Time time = new Time(DEFAULT_TIMEZONE); 422 time.parse3339(startDate); 423 begin = time.toMillis(false /* use isDst */); 424 time.parse3339(endDate); 425 end = time.toMillis(false /* use isDst */); 426 427 // Convert the instance date string to UTC milliseconds 428 time.parse3339(date); 429 allDay = time.allDay; 430 instance = time.toMillis(false /* use isDst */); 431 } 432 433 public void execute() { 434 Cursor cursor = queryInstances(begin, end); 435 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 436 boolean found = false; 437 while (cursor.moveToNext()) { 438 long begin = cursor.getLong(beginColumn); 439 440 if (instance == begin) { 441 found = true; 442 break; 443 } 444 } 445 if (!found) { 446 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE; 447 if (allDay) { 448 flags |= DateUtils.FORMAT_UTC; 449 } else { 450 flags |= DateUtils.FORMAT_SHOW_TIME; 451 } 452 String date = DateUtils.formatDateRange(mContext, instance, instance, flags); 453 String mesg = String.format("Test failed!" 454 + " cannot find instance at %s", 455 date); 456 Log.e(TAG, mesg); 457 } 458 assertTrue(found); 459 cursor.close(); 460 } 461 } 462 463 /** 464 * This class stores all the useful information about an event. 465 */ 466 private class EventInfo { 467 String mTitle; 468 String mDescription; 469 String mTimezone; 470 boolean mAllDay; 471 long mDtstart; 472 long mDtend; 473 String mRrule; 474 String mDuration; 475 String mOriginalTitle; 476 long mOriginalInstance; 477 int mSyncId; 478 479 // Constructor for normal events, using the default timezone 480 public EventInfo(String title, String startDate, String endDate, 481 boolean allDay) { 482 init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE); 483 } 484 485 // Constructor for normal events, specifying the timezone 486 public EventInfo(String title, String startDate, String endDate, 487 boolean allDay, String timezone) { 488 init(title, startDate, endDate, allDay, timezone); 489 } 490 491 public void init(String title, String startDate, String endDate, 492 boolean allDay, String timezone) { 493 mTitle = title; 494 Time time = new Time(); 495 if (allDay) { 496 time.timezone = Time.TIMEZONE_UTC; 497 } else if (timezone != null) { 498 time.timezone = timezone; 499 } 500 mTimezone = time.timezone; 501 time.parse3339(startDate); 502 mDtstart = time.toMillis(false /* use isDst */); 503 time.parse3339(endDate); 504 mDtend = time.toMillis(false /* use isDst */); 505 mDuration = null; 506 mRrule = null; 507 mAllDay = allDay; 508 } 509 510 // Constructor for repeating events, using the default timezone 511 public EventInfo(String title, String description, String startDate, String endDate, 512 String rrule, boolean allDay) { 513 init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE); 514 } 515 516 // Constructor for repeating events, specifying the timezone 517 public EventInfo(String title, String description, String startDate, String endDate, 518 String rrule, boolean allDay, String timezone) { 519 init(title, description, startDate, endDate, rrule, allDay, timezone); 520 } 521 522 public void init(String title, String description, String startDate, String endDate, 523 String rrule, boolean allDay, String timezone) { 524 mTitle = title; 525 mDescription = description; 526 Time time = new Time(); 527 if (allDay) { 528 time.timezone = Time.TIMEZONE_UTC; 529 } else if (timezone != null) { 530 time.timezone = timezone; 531 } 532 mTimezone = time.timezone; 533 time.parse3339(startDate); 534 mDtstart = time.toMillis(false /* use isDst */); 535 if (endDate != null) { 536 time.parse3339(endDate); 537 mDtend = time.toMillis(false /* use isDst */); 538 } 539 if (allDay) { 540 long days = 1; 541 if (endDate != null) { 542 days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS; 543 } 544 mDuration = "P" + days + "D"; 545 } else { 546 long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS; 547 mDuration = "P" + seconds + "S"; 548 } 549 mRrule = rrule; 550 mAllDay = allDay; 551 } 552 553 // Constructor for recurrence exceptions, using the default timezone 554 public EventInfo(String originalTitle, String originalInstance, String title, 555 String description, String startDate, String endDate, boolean allDay) { 556 init(originalTitle, originalInstance, 557 title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE); 558 } 559 560 public void init(String originalTitle, String originalInstance, 561 String title, String description, String startDate, String endDate, 562 boolean allDay, String timezone) { 563 mOriginalTitle = originalTitle; 564 Time time = new Time(timezone); 565 time.parse3339(originalInstance); 566 mOriginalInstance = time.toMillis(false /* use isDst */); 567 init(title, description, startDate, endDate, null /* rrule */, allDay, timezone); 568 } 569 } 570 571 private class InstanceInfo { 572 EventInfo mEvent; 573 long mBegin; 574 long mEnd; 575 int mExpectedOccurrences; 576 577 public InstanceInfo(String eventName, String startDate, String endDate, int expected) { 578 // Find the test index that contains the given event name 579 mEvent = findEvent(eventName); 580 Time time = new Time(mEvent.mTimezone); 581 time.parse3339(startDate); 582 mBegin = time.toMillis(false /* use isDst */); 583 time.parse3339(endDate); 584 mEnd = time.toMillis(false /* use isDst */); 585 mExpectedOccurrences = expected; 586 } 587 } 588 589 /** 590 * This is the main table of events. The events in this table are 591 * referred to by name in other places. 592 */ 593 private EventInfo[] mEvents = { 594 new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false), 595 new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false), 596 new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false), 597 new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true), 598 new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true), 599 new EventInfo("daily0", "daily from 5/1/2008 12am to 1am", 600 "2008-05-01T00:00:00", "2008-05-01T01:00:00", 601 "FREQ=DAILY;WKST=SU", false), 602 new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am", 603 "2008-05-01T08:30:00", "2008-05-01T09:30:00", 604 "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false), 605 new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am", 606 "2008-05-01T08:45:00", "2008-05-01T09:15:00", 607 "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false), 608 new EventInfo("allday daily0", "all-day daily from 5/1/2008", 609 "2008-05-01", null, 610 "FREQ=DAILY;WKST=SU", true), 611 new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008", 612 "2008-05-01", null, 613 "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true), 614 new EventInfo("allday weekly0", "all-day weekly from 5/1/2008", 615 "2008-05-01", null, 616 "FREQ=WEEKLY;WKST=SU", true), 617 new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008", 618 "2008-05-01", "2008-05-03", 619 "FREQ=WEEKLY;WKST=SU", true), 620 new EventInfo("allday yearly0", "all-day yearly on 5/1/2008", 621 "2008-05-01T", null, 622 "FREQ=YEARLY;WKST=SU", true), 623 new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm", 624 "2008-05-06T13:00:00", "2008-05-06T14:00:00", 625 "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false), 626 new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm", 627 "2008-05-06T14:30:00", "2008-05-06T15:30:00", 628 "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false), 629 new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm", 630 "2008-05-20T15:00:00", "2008-05-20T16:00:00", 631 "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false), 632 new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am", 633 "2008-05-01T00:00:00", "2008-05-01T00:10:00", 634 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false), 635 new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight", 636 "2008-05-31T23:00:00", "2008-06-01T00:00:00", 637 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false), 638 new EventInfo("daily0", "2008-05-01T00:00:00", 639 "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am", 640 "2008-05-01T02:00:00", "2008-05-01T01:03:00", false), 641 new EventInfo("daily0", "2008-05-03T00:00:00", 642 "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am", 643 "2008-05-03T02:00:00", "2008-05-03T01:03:00", false), 644 new EventInfo("daily0", "2008-05-02T00:00:00", 645 "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008", 646 "2008-01-02T00:00:00", "2008-01-02T01:00:00", false), 647 new EventInfo("weekly0", "2008-05-13T13:00:00", 648 "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm", 649 "2008-12-11T13:00:00", "2008-12-11T14:00:00", false), 650 new EventInfo("weekly0", "2008-05-13T13:00:00", 651 "cancel0", "weekly0 exception for 5/13/2008 1pm", 652 "2008-05-13T13:00:00", "2008-05-13T14:00:00", false), 653 new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm", 654 "2008-05-01T13:00:00", "2008-05-01T14:00:00", 655 "FREQ=YEARLY;WKST=SU", false), 656 }; 657 658 /** 659 * This table is used to create repeating events and then check that the 660 * number of instances within a given range matches the expected number 661 * of instances. 662 */ 663 private InstanceInfo[] mInstanceRanges = { 664 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1), 665 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 666 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2), 667 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2), 668 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 669 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1), 670 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2), 671 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31), 672 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32), 673 674 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 675 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2), 676 677 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 678 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3), 679 680 new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7), 681 new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3), 682 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1), 683 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2), 684 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5), 685 new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5), 686 new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1), 687 new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2), 688 689 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 690 new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 691 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4), 692 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8), 693 694 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 695 new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 696 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2), 697 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4), 698 699 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0), 700 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1), 701 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0), 702 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0), 703 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1), 704 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 705 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2), 706 707 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 708 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 709 new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1), 710 new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0), 711 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2), 712 713 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0), 714 new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1), 715 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1), 716 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2), 717 718 new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1), 719 new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2), 720 }; 721 722 /** 723 * This sequence of commands inserts and deletes some events. 724 */ 725 private Command[] mNormalInsertDelete = { 726 new Insert("normal0"), 727 new Insert("normal1"), 728 new Insert("normal2"), 729 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3), 730 new Delete("normal1", 1), 731 new QueryNumEvents(2), 732 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2), 733 new Delete("normal1", 0), 734 new Delete("normal2", 1), 735 new QueryNumEvents(1), 736 new Delete("normal0", 1), 737 new QueryNumEvents(0), 738 }; 739 740 /** 741 * This sequence of commands inserts and deletes some all-day events. 742 */ 743 private Command[] mAlldayInsertDelete = { 744 new Insert("allday0"), 745 new Insert("allday1"), 746 new QueryNumEvents(2), 747 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0), 748 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2), 749 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 750 new Delete("allday0", 1), 751 new QueryNumEvents(1), 752 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 753 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 754 new Delete("allday1", 1), 755 new QueryNumEvents(0), 756 }; 757 758 /** 759 * This sequence of commands inserts and deletes some repeating events. 760 */ 761 private Command[] mRecurringInsertDelete = { 762 new Insert("daily0"), 763 new Insert("daily1"), 764 new QueryNumEvents(2), 765 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3), 766 new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2), 767 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6), 768 new Delete("daily1", 1), 769 new QueryNumEvents(1), 770 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2), 771 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4), 772 new Delete("daily0", 1), 773 new QueryNumEvents(0), 774 }; 775 776 /** 777 * This sequence of commands creates a recurring event with a recurrence 778 * exception that moves an event outside the expansion window. It checks that the 779 * recurrence exception does not occur in the Instances database table. 780 * Bug 1642665 781 */ 782 private Command[] mExceptionWithMovedRecurrence = { 783 new Insert("daily0"), 784 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 785 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 786 "2008-05-03T00:00:00", }), 787 new Insert("except2"), 788 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 789 new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}), 790 }; 791 792 /** 793 * This sequence of commands deletes (cancels) one instance of a recurrence. 794 */ 795 private Command[] mCancelInstance = { 796 new Insert("weekly0"), 797 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 798 new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00", 799 "2008-05-20T13:00:00", }), 800 new Insert("cancel0"), 801 new Update("cancel0", new KeyValue[] { 802 new KeyValue(Calendar.EventsColumns.STATUS, 803 "" + Calendar.EventsColumns.STATUS_CANCELED), 804 }), 805 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 806 new String[] {"2008-05-06T13:00:00", 807 "2008-05-20T13:00:00", }), 808 }; 809 /** 810 * This sequence of commands creates a recurring event with a recurrence 811 * exception that moves an event from outside the expansion window into the 812 * expansion window. 813 */ 814 private Command[] mExceptionWithMovedRecurrence2 = { 815 new Insert("weekly0"), 816 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 817 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 818 "2008-12-16T13:00:00", }), 819 new Insert("except3"), 820 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 821 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 822 "2008-12-11T13:00:00", "2008-12-16T13:00:00", }), 823 }; 824 /** 825 * This sequence of commands creates a recurring event with a recurrence 826 * exception and then changes the end time of the recurring event. It then 827 * checks that the recurrence exception does not occur in the Instances 828 * database table. 829 */ 830 private Command[] 831 mExceptionWithTruncatedRecurrence = { 832 new Insert("daily0"), 833 // Verify 4 occurrences of the "daily0" repeating event 834 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 835 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 836 "2008-05-03T00:00:00", "2008-05-04T00:00:00"}), 837 new Insert("except1"), 838 new QueryNumEvents(2), 839 840 // Verify that one of the 4 occurrences has its start time changed 841 // so that it now matches the recurrence exception. 842 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 843 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 844 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 845 846 // Change the end time of "daily0" but it still includes the 847 // recurrence exception. 848 new Update("daily0", new KeyValue[] { 849 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"), 850 }), 851 852 // Verify that the recurrence exception is still there 853 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 854 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 855 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 856 // This time change the end time of "daily0" so that it excludes 857 // the recurrence exception. 858 new Update("daily0", new KeyValue[] { 859 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"), 860 }), 861 // The server will cancel the out-of-range exception. 862 // It would be nice for the provider to handle this automatically, 863 // but for now simulate the server-side cancel. 864 new Update("except1", new KeyValue[] { 865 new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED), 866 }), 867 // Verify that the recurrence exception does not appear. 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 }; 871 872 /** 873 * Bug 135848. Ensure that a recurrence exception is displayed even if the recurrence 874 * is not present. 875 */ 876 private Command[] mExceptionWithNoRecurrence = { 877 new Insert("except0"), 878 new QueryNumEvents(1), 879 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 880 new String[] {"2008-05-01T02:00:00"}), 881 }; 882 883 private EventInfo findEvent(String name) { 884 int len = mEvents.length; 885 for (int ii = 0; ii < len; ii++) { 886 EventInfo event = mEvents[ii]; 887 if (name.equals(event.mTitle)) { 888 return event; 889 } 890 } 891 return null; 892 } 893 894 @Override 895 protected void setUp() throws Exception { 896 super.setUp(); 897 // This code here is the code that was originally in ProviderTestCase2 898 mResolver = new MockContentResolver(); 899 900 final String filenamePrefix = "test."; 901 RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext( 902 new MockContext2(), // The context that most methods are delegated to 903 getContext(), // The context that file methods are delegated to 904 filenamePrefix); 905 mContext = new IsolatedContext(mResolver, targetContextWrapper); 906 907 mProvider = new CalendarProvider2ForTesting(); 908 mProvider.attachInfo(mContext, null); 909 910 mResolver.addProvider(Calendar.AUTHORITY, mProvider); 911 mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds")); 912 mResolver.addProvider("sync", new MockProvider("sync")); 913 914 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 915 mDb = helper.getWritableDatabase(); 916 wipeData(mDb); 917 mMetaData = getProvider().mMetaData; 918 mForceDtend = false; 919 } 920 921 @Override 922 protected void tearDown() throws Exception { 923 try { 924 mDb.close(); 925 mDb = null; 926 getProvider().getDatabaseHelper().close(); 927 } catch (IllegalStateException e) { 928 e.printStackTrace(); 929 } 930 super.tearDown(); 931 } 932 933 public void wipeData(SQLiteDatabase db) { 934 db.execSQL("DELETE FROM Calendars;"); 935 db.execSQL("DELETE FROM Events;"); 936 db.execSQL("DELETE FROM EventsRawTimes;"); 937 db.execSQL("DELETE FROM Instances;"); 938 db.execSQL("DELETE FROM CalendarMetaData;"); 939 db.execSQL("DELETE FROM CalendarCache;"); 940 db.execSQL("DELETE FROM Attendees;"); 941 db.execSQL("DELETE FROM Reminders;"); 942 db.execSQL("DELETE FROM CalendarAlerts;"); 943 db.execSQL("DELETE FROM ExtendedProperties;"); 944 } 945 946 protected CalendarProvider2ForTesting getProvider() { 947 return mProvider; 948 } 949 950 /** 951 * Dumps the contents of the given cursor to the log. For debugging. 952 * @param cursor the database cursor 953 */ 954 private void dumpCursor(Cursor cursor) { 955 cursor.moveToPosition(-1); 956 String[] cols = cursor.getColumnNames(); 957 958 Log.i(TAG, "dumpCursor() count: " + cursor.getCount()); 959 int index = 0; 960 while (cursor.moveToNext()) { 961 Log.i(TAG, index + " {"); 962 for (int i = 0; i < cols.length; i++) { 963 Log.i(TAG, " " + cols[i] + '=' + cursor.getString(i)); 964 } 965 Log.i(TAG, "}"); 966 index += 1; 967 } 968 cursor.moveToPosition(-1); 969 } 970 971 private int insertCal(String name, String timezone) { 972 return insertCal(name, timezone, "joe@joe.com"); 973 } 974 975 private int insertCal(String name, String timezone, String account) { 976 ContentValues m = new ContentValues(); 977 m.put(Calendars.NAME, name); 978 m.put(Calendars.DISPLAY_NAME, name); 979 m.put(Calendars.COLOR, "0xff123456"); 980 m.put(Calendars.TIMEZONE, timezone); 981 m.put(Calendars.SELECTED, 1); 982 m.put(Calendars.SYNC1, CALENDAR_URL); 983 m.put(Calendars.OWNER_ACCOUNT, account); 984 m.put(Calendars._SYNC_ACCOUNT, account); 985 m.put(Calendars._SYNC_ACCOUNT_TYPE, "com.google"); 986 m.put(Calendars.SYNC_EVENTS, 1); 987 988 Uri url = mResolver.insert(Calendar.Calendars.CONTENT_URI, m); 989 String id = url.getLastPathSegment(); 990 return Integer.parseInt(id); 991 } 992 993 private Uri insertEvent(int calId, EventInfo event) { 994 if (mWipe) { 995 // Wipe instance table so it will be regenerated 996 mMetaData.clearInstanceRange(); 997 } 998 ContentValues m = new ContentValues(); 999 m.put(Events.CALENDAR_ID, calId); 1000 m.put(Events.TITLE, event.mTitle); 1001 m.put(Events.DTSTART, event.mDtstart); 1002 m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0); 1003 1004 if (event.mRrule == null || mForceDtend) { 1005 // This is a normal event 1006 m.put(Events.DTEND, event.mDtend); 1007 } 1008 if (event.mRrule != null) { 1009 // This is a repeating event 1010 m.put(Events.RRULE, event.mRrule); 1011 m.put(Events.DURATION, event.mDuration); 1012 } 1013 1014 if (event.mDescription != null) { 1015 m.put(Events.DESCRIPTION, event.mDescription); 1016 } 1017 if (event.mTimezone != null) { 1018 m.put(Events.EVENT_TIMEZONE, event.mTimezone); 1019 } 1020 1021 if (event.mOriginalTitle != null) { 1022 // This is a recurrence exception. 1023 EventInfo recur = findEvent(event.mOriginalTitle); 1024 assertNotNull(recur); 1025 String syncId = String.format("%d", recur.mSyncId); 1026 m.put(Events.ORIGINAL_EVENT, syncId); 1027 m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0); 1028 m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance); 1029 } 1030 Uri url = mResolver.insert(mEventsUri, m); 1031 1032 // Create a fake _sync_id and add it to the event. Update the database 1033 // directly so that we don't trigger any validation checks in the 1034 // CalendarProvider. 1035 long id = ContentUris.parseId(url); 1036 mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id); 1037 event.mSyncId = mGlobalSyncId; 1038 mGlobalSyncId += 1; 1039 1040 return url; 1041 } 1042 1043 /** 1044 * Deletes all the events that match the given title. 1045 * @param title the given title to match events on 1046 * @return the number of rows deleted 1047 */ 1048 private int deleteMatchingEvents(String title) { 1049 Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID }, 1050 "title=?", new String[] { title }, null); 1051 int numRows = 0; 1052 while (cursor.moveToNext()) { 1053 long id = cursor.getLong(0); 1054 // Do delete as a sync adapter so event is really deleted, not just marked 1055 // as deleted. 1056 Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true); 1057 numRows += mResolver.delete(uri, null, null); 1058 } 1059 cursor.close(); 1060 return numRows; 1061 } 1062 1063 /** 1064 * Updates all the events that match the given title. 1065 * @param title the given title to match events on 1066 * @return the number of rows updated 1067 */ 1068 private int updateMatchingEvents(String title, ContentValues values) { 1069 String[] projection = new String[] { 1070 Events._ID, 1071 Events.DTSTART, 1072 Events.DTEND, 1073 Events.DURATION, 1074 Events.ALL_DAY, 1075 Events.RRULE, 1076 Events.EVENT_TIMEZONE, 1077 Events.ORIGINAL_EVENT, 1078 }; 1079 Cursor cursor = mResolver.query(mEventsUri, projection, 1080 "title=?", new String[] { title }, null); 1081 int numRows = 0; 1082 while (cursor.moveToNext()) { 1083 long id = cursor.getLong(0); 1084 1085 // If any of the following fields are being changed, then we need 1086 // to include all of them. 1087 if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND) 1088 || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY) 1089 || values.containsKey(Events.RRULE) 1090 || values.containsKey(Events.EVENT_TIMEZONE) 1091 || values.containsKey(Calendar.EventsColumns.STATUS)) { 1092 long dtstart = cursor.getLong(1); 1093 long dtend = cursor.getLong(2); 1094 String duration = cursor.getString(3); 1095 boolean allDay = cursor.getInt(4) != 0; 1096 String rrule = cursor.getString(5); 1097 String timezone = cursor.getString(6); 1098 String originalEvent = cursor.getString(7); 1099 1100 if (!values.containsKey(Events.DTSTART)) { 1101 values.put(Events.DTSTART, dtstart); 1102 } 1103 // Don't add DTEND for repeating events 1104 if (!values.containsKey(Events.DTEND) && rrule == null) { 1105 values.put(Events.DTEND, dtend); 1106 } 1107 if (!values.containsKey(Events.DURATION) && duration != null) { 1108 values.put(Events.DURATION, duration); 1109 } 1110 if (!values.containsKey(Events.ALL_DAY)) { 1111 values.put(Events.ALL_DAY, allDay ? 1 : 0); 1112 } 1113 if (!values.containsKey(Events.RRULE) && rrule != null) { 1114 values.put(Events.RRULE, rrule); 1115 } 1116 if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) { 1117 values.put(Events.EVENT_TIMEZONE, timezone); 1118 } 1119 if (!values.containsKey(Events.ORIGINAL_EVENT) && originalEvent != null) { 1120 values.put(Events.ORIGINAL_EVENT, originalEvent); 1121 } 1122 } 1123 1124 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 1125 numRows += mResolver.update(uri, values, null, null); 1126 } 1127 cursor.close(); 1128 return numRows; 1129 } 1130 1131 private void deleteAllEvents() { 1132 mDb.execSQL("DELETE FROM Events;"); 1133 mMetaData.clearInstanceRange(); 1134 } 1135 1136 public void testInsertNormalEvents() throws Exception { 1137 Cursor cursor; 1138 Uri url = null; 1139 1140 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1141 1142 cursor = mResolver.query(mEventsUri, null, null, null, null); 1143 assertEquals(0, cursor.getCount()); 1144 cursor.close(); 1145 1146 // Keep track of the number of normal events 1147 int numEvents = 0; 1148 1149 // "begin" is the earliest start time of all the normal events, 1150 // and "end" is the latest end time of all the normal events. 1151 long begin = 0, end = 0; 1152 1153 int len = mEvents.length; 1154 for (int ii = 0; ii < len; ii++) { 1155 EventInfo event = mEvents[ii]; 1156 // Skip repeating events and recurrence exceptions 1157 if (event.mRrule != null || event.mOriginalTitle != null) { 1158 continue; 1159 } 1160 if (numEvents == 0) { 1161 begin = event.mDtstart; 1162 end = event.mDtend; 1163 } else { 1164 if (begin > event.mDtstart) { 1165 begin = event.mDtstart; 1166 } 1167 if (end < event.mDtend) { 1168 end = event.mDtend; 1169 } 1170 } 1171 url = insertEvent(calId, event); 1172 numEvents += 1; 1173 } 1174 1175 // query one 1176 cursor = mResolver.query(url, null, null, null, null); 1177 assertEquals(1, cursor.getCount()); 1178 cursor.close(); 1179 1180 // query all 1181 cursor = mResolver.query(mEventsUri, null, null, null, null); 1182 assertEquals(numEvents, cursor.getCount()); 1183 cursor.close(); 1184 1185 // Check that the Instances table has one instance of each of the 1186 // normal events. 1187 cursor = queryInstances(begin, end); 1188 assertEquals(numEvents, cursor.getCount()); 1189 cursor.close(); 1190 } 1191 1192 public void testInsertRepeatingEvents() throws Exception { 1193 Cursor cursor; 1194 Uri url = null; 1195 1196 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1197 1198 cursor = mResolver.query(mEventsUri, null, null, null, null); 1199 assertEquals(0, cursor.getCount()); 1200 cursor.close(); 1201 1202 // Keep track of the number of repeating events 1203 int numEvents = 0; 1204 1205 int len = mEvents.length; 1206 for (int ii = 0; ii < len; ii++) { 1207 EventInfo event = mEvents[ii]; 1208 // Skip normal events 1209 if (event.mRrule == null) { 1210 continue; 1211 } 1212 url = insertEvent(calId, event); 1213 numEvents += 1; 1214 } 1215 1216 // query one 1217 cursor = mResolver.query(url, null, null, null, null); 1218 assertEquals(1, cursor.getCount()); 1219 cursor.close(); 1220 1221 // query all 1222 cursor = mResolver.query(mEventsUri, null, null, null, null); 1223 assertEquals(numEvents, cursor.getCount()); 1224 cursor.close(); 1225 } 1226 1227 // Force a dtend value to be set and make sure instance expansion still works 1228 public void testInstanceRangeDtend() throws Exception { 1229 mForceDtend = true; 1230 testInstanceRange(); 1231 } 1232 1233 public void testInstanceRange() throws Exception { 1234 Cursor cursor; 1235 Uri url = null; 1236 1237 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1238 1239 cursor = mResolver.query(mEventsUri, null, null, null, null); 1240 assertEquals(0, cursor.getCount()); 1241 cursor.close(); 1242 1243 int len = mInstanceRanges.length; 1244 for (int ii = 0; ii < len; ii++) { 1245 InstanceInfo instance = mInstanceRanges[ii]; 1246 EventInfo event = instance.mEvent; 1247 url = insertEvent(calId, event); 1248 cursor = queryInstances(instance.mBegin, instance.mEnd); 1249 if (instance.mExpectedOccurrences != cursor.getCount()) { 1250 Log.e(TAG, "Test failed! Instance index: " + ii); 1251 Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription 1252 + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]" 1253 + " expected: " + instance.mExpectedOccurrences); 1254 dumpCursor(cursor); 1255 } 1256 assertEquals(instance.mExpectedOccurrences, cursor.getCount()); 1257 cursor.close(); 1258 // Delete as sync_adapter so event is really deleted. 1259 int rows = mResolver.delete(updatedUri(url, true), 1260 null /* selection */, null /* selection args */); 1261 assertEquals(1, rows); 1262 } 1263 } 1264 1265 public static <T> void assertArrayEquals(T[] expected, T[] actual) { 1266 if (!Arrays.equals(expected, actual)) { 1267 fail("expected:<" + Arrays.toString(expected) + 1268 "> but was:<" + Arrays.toString(actual) + ">"); 1269 } 1270 } 1271 1272 @SmallTest @Smoke 1273 public void testTokenizeSearchQuery() { 1274 String query = ""; 1275 String[] expectedTokens = new String[] {""}; 1276 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1277 1278 query = "a"; 1279 expectedTokens = new String[] {"a"}; 1280 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1281 1282 query = "two words"; 1283 expectedTokens = new String[] {"two", "words"}; 1284 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1285 1286 query = "test, punctuation."; 1287 expectedTokens = new String[] {"test", "punctuation"}; 1288 assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); 1289 } 1290 1291 @SmallTest @Smoke 1292 public void testConstructSearchWhere() { 1293 String[] tokens = new String[] {"red"}; 1294 String expected = "(title LIKE ? OR description LIKE ? OR eventLocation" 1295 + " LIKE ? ) AND "; 1296 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1297 1298 tokens = new String[] {}; 1299 expected = ""; 1300 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1301 1302 tokens = new String[] {"red", "green"}; 1303 expected = "(title LIKE ? OR description LIKE ? OR eventLocation" 1304 + " LIKE ? ) AND (title LIKE ? OR description LIKE ? OR " 1305 + "eventLocation LIKE ? ) AND "; 1306 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1307 1308 tokens = new String[] {"red blue", "green"}; 1309 expected = "(title LIKE ? OR description LIKE ? OR eventLocation" 1310 + " LIKE ? ) AND (title LIKE ? OR description LIKE ? OR " 1311 + "eventLocation LIKE ? ) AND "; 1312 assertEquals(expected, mProvider.constructSearchWhere(tokens)); 1313 } 1314 1315 @SmallTest @Smoke 1316 public void testConstructSearchArgs() { 1317 long rangeBegin = 0; 1318 long rangeEnd = 10; 1319 1320 String[] tokens = new String[] {"red"}; 1321 String[] expected = new String[] {"%red%", "%red%", "%red%", "10", "0" }; 1322 assertArrayEquals(expected, mProvider.constructSearchArgs(tokens, 1323 rangeBegin, rangeEnd)); 1324 1325 tokens = new String[] {"red", "blue"}; 1326 expected = new String[] {"%red%", "%red%", "%red%", 1327 "%blue%", "%blue%","%blue%", "10", "0" }; 1328 assertArrayEquals(expected, mProvider.constructSearchArgs(tokens, 1329 rangeBegin, rangeEnd)); 1330 1331 tokens = new String[] {}; 1332 expected = new String[] {"10", "0" }; 1333 assertArrayEquals(expected, mProvider.constructSearchArgs(tokens, 1334 rangeBegin, rangeEnd)); 1335 } 1336 1337 public void testInstanceSearchQuery() throws Exception { 1338 final String[] PROJECTION = new String[] { 1339 Instances.TITLE, // 0 1340 Instances.EVENT_LOCATION, // 1 1341 Instances.ALL_DAY, // 2 1342 Instances.COLOR, // 3 1343 Instances.EVENT_TIMEZONE, // 4 1344 Instances.EVENT_ID, // 5 1345 Instances.BEGIN, // 6 1346 Instances.END, // 7 1347 Instances._ID, // 8 1348 Instances.START_DAY, // 9 1349 Instances.END_DAY, // 10 1350 Instances.START_MINUTE, // 11 1351 Instances.END_MINUTE, // 12 1352 Instances.HAS_ALARM, // 13 1353 Instances.RRULE, // 14 1354 Instances.RDATE, // 15 1355 Instances.SELF_ATTENDEE_STATUS, // 16 1356 Events.ORGANIZER, // 17 1357 Events.GUESTS_CAN_MODIFY, // 18 1358 }; 1359 1360 String orderBy = Instances.SORT_CALENDAR_VIEW; 1361 String where = Instances.SELF_ATTENDEE_STATUS + "!=" + 1362 Calendar.Attendees.ATTENDEE_STATUS_DECLINED; 1363 1364 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1365 final String START = "2008-05-01T00:00:00"; 1366 final String END = "2008-05-01T20:00:00"; 1367 1368 EventInfo event1 = new EventInfo("search orange", 1369 START, 1370 END, 1371 false /* allDay */, 1372 DEFAULT_TIMEZONE); 1373 event1.mDescription = "this is description1"; 1374 1375 EventInfo event2 = new EventInfo("search purple", 1376 START, 1377 END, 1378 false /* allDay */, 1379 DEFAULT_TIMEZONE); 1380 event2.mDescription = "lasers, out of nowhere"; 1381 1382 EventInfo event3 = new EventInfo("", 1383 START, 1384 END, 1385 false /* allDay */, 1386 DEFAULT_TIMEZONE); 1387 event3.mDescription = "kapow"; 1388 1389 EventInfo[] events = { event1, event2, event3 }; 1390 1391 insertEvent(calId, events[0]); 1392 insertEvent(calId, events[1]); 1393 insertEvent(calId, events[2]); 1394 1395 Time time = new Time(DEFAULT_TIMEZONE); 1396 time.parse3339(START); 1397 long startMs = time.toMillis(true /* ignoreDst */); 1398 // Query starting from way in the past to one hour into the event. 1399 // Query is more than 2 months so the range won't get extended by the provider. 1400 Cursor cursor = null; 1401 1402 try { 1403 cursor = Instances.query(mResolver, PROJECTION, 1404 startMs - DateUtils.YEAR_IN_MILLIS, 1405 startMs + DateUtils.HOUR_IN_MILLIS, 1406 "search", where, orderBy); 1407 assertEquals(2, cursor.getCount()); 1408 } finally { 1409 if (cursor != null) { 1410 cursor.close(); 1411 } 1412 } 1413 1414 try { 1415 cursor = Instances.query(mResolver, PROJECTION, 1416 startMs - DateUtils.YEAR_IN_MILLIS, 1417 startMs + DateUtils.HOUR_IN_MILLIS, 1418 "purple", where, orderBy); 1419 assertEquals(1, cursor.getCount()); 1420 } finally { 1421 if (cursor != null) { 1422 cursor.close(); 1423 } 1424 } 1425 1426 try { 1427 cursor = Instances.query(mResolver, PROJECTION, 1428 startMs - DateUtils.YEAR_IN_MILLIS, 1429 startMs + DateUtils.HOUR_IN_MILLIS, 1430 "puurple", where, orderBy); 1431 assertEquals(0, cursor.getCount()); 1432 } finally { 1433 if (cursor != null) { 1434 cursor.close(); 1435 } 1436 } 1437 1438 try { 1439 cursor = Instances.query(mResolver, PROJECTION, 1440 startMs - DateUtils.YEAR_IN_MILLIS, 1441 startMs + DateUtils.HOUR_IN_MILLIS, 1442 "purple lasers", where, orderBy); 1443 assertEquals(1, cursor.getCount()); 1444 } finally { 1445 if (cursor != null) { 1446 cursor.close(); 1447 } 1448 } 1449 1450 try { 1451 cursor = Instances.query(mResolver, PROJECTION, 1452 startMs - DateUtils.YEAR_IN_MILLIS, 1453 startMs + DateUtils.HOUR_IN_MILLIS, 1454 "lasers kapow", where, orderBy); 1455 assertEquals(0, cursor.getCount()); 1456 } finally { 1457 if (cursor != null) { 1458 cursor.close(); 1459 } 1460 } 1461 } 1462 1463 public void testEntityQuery() throws Exception { 1464 testInsertNormalEvents(); // To initialize 1465 1466 ContentValues reminder = new ContentValues(); 1467 reminder.put(Calendar.Reminders.EVENT_ID, 1); 1468 reminder.put(Calendar.Reminders.MINUTES, 10); 1469 reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_SMS); 1470 mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder); 1471 reminder.put(Calendar.Reminders.MINUTES, 20); 1472 mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder); 1473 1474 ContentValues extended = new ContentValues(); 1475 extended.put(Calendar.ExtendedProperties.NAME, "foo"); 1476 extended.put(Calendar.ExtendedProperties.VALUE, "bar"); 1477 extended.put(Calendar.ExtendedProperties.EVENT_ID, 2); 1478 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1479 extended.put(Calendar.ExtendedProperties.EVENT_ID, 1); 1480 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1481 extended.put(Calendar.ExtendedProperties.NAME, "foo2"); 1482 extended.put(Calendar.ExtendedProperties.VALUE, "bar2"); 1483 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1484 1485 ContentValues attendee = new ContentValues(); 1486 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1487 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1488 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1489 Calendar.Attendees.ATTENDEE_STATUS_DECLINED); 1490 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1491 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1492 Calendar.Attendees.RELATIONSHIP_PERFORMER); 1493 attendee.put(Calendar.Attendees.EVENT_ID, 3); 1494 mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1495 1496 EntityIterator ei = EventsEntity.newEntityIterator( 1497 mResolver.query(EventsEntity.CONTENT_URI, null, null, null, null), mResolver); 1498 int count = 0; 1499 try { 1500 while (ei.hasNext()) { 1501 Entity entity = ei.next(); 1502 ContentValues values = entity.getEntityValues(); 1503 assertEquals(CALENDAR_URL, values.getAsString(Calendars.SYNC1)); 1504 ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues(); 1505 switch (values.getAsInteger("_id")) { 1506 case 1: 1507 assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties 1508 break; 1509 case 2: 1510 assertEquals(1, subvalues.size()); // Extended properties 1511 ContentValues subContentValues = subvalues.get(0).values; 1512 String name = subContentValues.getAsString( 1513 Calendar.ExtendedProperties.NAME); 1514 String value = subContentValues.getAsString( 1515 Calendar.ExtendedProperties.VALUE); 1516 assertEquals("foo", name); 1517 assertEquals("bar", value); 1518 break; 1519 case 3: 1520 assertEquals(1, subvalues.size()); // Attendees 1521 break; 1522 default: 1523 assertEquals(0, subvalues.size()); 1524 break; 1525 } 1526 count += 1; 1527 } 1528 assertEquals(5, count); 1529 } finally { 1530 ei.close(); 1531 } 1532 1533 ei = EventsEntity.newEntityIterator( 1534 mResolver.query(EventsEntity.CONTENT_URI, null, "_id = 3", null, null), 1535 mResolver); 1536 try { 1537 count = 0; 1538 while (ei.hasNext()) { 1539 Entity entity = ei.next(); 1540 count += 1; 1541 } 1542 assertEquals(1, count); 1543 } finally { 1544 ei.close(); 1545 } 1546 } 1547 1548 public void testDeleteCalendar() throws Exception { 1549 int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); 1550 int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com"); 1551 insertEvent(calendarId0, mEvents[0]); 1552 insertEvent(calendarId1, mEvents[1]); 1553 // Should have 2 calendars and 2 events 1554 testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 2); 1555 testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 2); 1556 1557 int deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI, 1558 "ownerAccount='user2@google.com'", null /* selectionArgs */); 1559 1560 assertEquals(1, deletes); 1561 // Should have 1 calendar and 1 event 1562 testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 1); 1563 testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 1); 1564 1565 deletes = mResolver.delete(Uri.withAppendedPath(Calendar.Calendars.CONTENT_URI, 1566 String.valueOf(calendarId0)), 1567 null /* selection*/ , null /* selectionArgs */); 1568 1569 assertEquals(1, deletes); 1570 // Should have 0 calendars and 0 events 1571 testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 0); 1572 testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 0); 1573 1574 deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI, 1575 "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */); 1576 1577 assertEquals(0, deletes); 1578 } 1579 1580 public void testCalendarAlerts() throws Exception { 1581 // This projection is from AlertActivity; want to make sure it works. 1582 String[] projection = new String[] { 1583 Calendar.CalendarAlerts._ID, // 0 1584 Calendar.CalendarAlerts.TITLE, // 1 1585 Calendar.CalendarAlerts.EVENT_LOCATION, // 2 1586 Calendar.CalendarAlerts.ALL_DAY, // 3 1587 Calendar.CalendarAlerts.BEGIN, // 4 1588 Calendar.CalendarAlerts.END, // 5 1589 Calendar.CalendarAlerts.EVENT_ID, // 6 1590 Calendar.CalendarAlerts.COLOR, // 7 1591 Calendar.CalendarAlerts.RRULE, // 8 1592 Calendar.CalendarAlerts.HAS_ALARM, // 9 1593 Calendar.CalendarAlerts.STATE, // 10 1594 Calendar.CalendarAlerts.ALARM_TIME, // 11 1595 }; 1596 testInsertNormalEvents(); // To initialize 1597 1598 Uri alertUri = Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */, 1599 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */); 1600 Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */, 1601 2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */); 1602 1603 // Regular query 1604 Cursor cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI, projection, 1605 null /* selection */, null /* selectionArgs */, null /* sortOrder */); 1606 1607 assertEquals(2, cursor.getCount()); 1608 cursor.close(); 1609 1610 // Instance query 1611 cursor = mResolver.query(alertUri, projection, 1612 null /* selection */, null /* selectionArgs */, null /* sortOrder */); 1613 1614 assertEquals(1, cursor.getCount()); 1615 cursor.close(); 1616 1617 // Grouped by event query 1618 cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI_BY_INSTANCE, projection, 1619 null /* selection */, null /* selectionArgs */, null /* sortOrder */); 1620 1621 assertEquals(1, cursor.getCount()); 1622 cursor.close(); 1623 } 1624 1625 /** 1626 * Test attendee processing 1627 * @throws Exception 1628 */ 1629 public void testAttendees() throws Exception { 1630 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1631 1632 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 1633 long eventId = ContentUris.parseId(eventUri); 1634 1635 ContentValues attendee = new ContentValues(); 1636 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1637 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1638 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1639 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1640 Calendar.Attendees.RELATIONSHIP_ORGANIZER); 1641 attendee.put(Calendar.Attendees.EVENT_ID, eventId); 1642 Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1643 1644 Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null, 1645 "event_id=" + eventId, null, null); 1646 assertEquals("Created event is missing", 1, cursor.getCount()); 1647 cursor.close(); 1648 1649 cursor = mResolver.query(eventUri, null, null, null, null); 1650 assertEquals("Created event is missing", 1, cursor.getCount()); 1651 int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS); 1652 cursor.moveToNext(); 1653 long selfAttendeeStatus = cursor.getInt(selfColumn); 1654 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus); 1655 cursor.close(); 1656 1657 // Change status to declined 1658 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1659 Calendar.Attendees.ATTENDEE_STATUS_DECLINED); 1660 mResolver.update(attendeesUri, attendee, null, null); 1661 1662 cursor = mResolver.query(eventUri, null, null, null, null); 1663 cursor.moveToNext(); 1664 selfAttendeeStatus = cursor.getInt(selfColumn); 1665 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 1666 cursor.close(); 1667 1668 // Add another attendee 1669 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude"); 1670 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com"); 1671 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1672 Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED); 1673 mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1674 1675 cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null, 1676 "event_id=" + mCalendarId, null, null); 1677 assertEquals(2, cursor.getCount()); 1678 cursor.close(); 1679 1680 cursor = mResolver.query(eventUri, null, null, null, null); 1681 cursor.moveToNext(); 1682 selfAttendeeStatus = cursor.getInt(selfColumn); 1683 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 1684 cursor.close(); 1685 } 1686 1687 1688 /** 1689 * Test the event's _sync_dirty status and clear it. 1690 * @param eventId event to fetch. 1691 * @param wanted the wanted _sync_dirty status 1692 */ 1693 private void testAndClearDirty(long eventId, int wanted) { 1694 Cursor cursor = mResolver.query( 1695 ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId), 1696 null, null, null, null); 1697 try { 1698 assertEquals("Event count", 1, cursor.getCount()); 1699 cursor.moveToNext(); 1700 int dirty = cursor.getInt(cursor.getColumnIndex(Calendar.Events._SYNC_DIRTY)); 1701 assertEquals("dirty flag", wanted, dirty); 1702 if (dirty == 1) { 1703 // Have to access database directly since provider will set dirty again. 1704 mDb.execSQL("UPDATE Events SET _sync_dirty=0 WHERE _id=" + eventId); 1705 } 1706 } finally { 1707 cursor.close(); 1708 } 1709 } 1710 1711 /** 1712 * Test the count of results from a query. 1713 * @param uri The URI to query 1714 * @param where The where string or null. 1715 * @param wanted The number of results wanted. An assertion is thrown if it doesn't match. 1716 */ 1717 private void testQueryCount(Uri uri, String where, int wanted) { 1718 Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */, 1719 null /* sortOrder */); 1720 try { 1721 assertEquals("query results", wanted, cursor.getCount()); 1722 } finally { 1723 cursor.close(); 1724 } 1725 } 1726 1727 /** 1728 * Test dirty flag processing. 1729 * @throws Exception 1730 */ 1731 public void testDirty() throws Exception { 1732 internalTestDirty(false); 1733 } 1734 1735 /** 1736 * Test dirty flag processing for updates from a sync adapter. 1737 * @throws Exception 1738 */ 1739 public void testDirtyWithSyncAdapter() throws Exception { 1740 internalTestDirty(true); 1741 } 1742 1743 /** 1744 * Add CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation. 1745 */ 1746 private Uri updatedUri(Uri uri, boolean syncAdapter) { 1747 if (syncAdapter) { 1748 return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true") 1749 .build(); 1750 } else { 1751 return uri; 1752 } 1753 } 1754 1755 /** 1756 * Test dirty flag processing either for syncAdapter operations or client operations. 1757 * The main difference is syncAdapter operations don't set the dirty bit. 1758 */ 1759 private void internalTestDirty(boolean syncAdapter) throws Exception { 1760 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1761 1762 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 1763 1764 long eventId = ContentUris.parseId(eventUri); 1765 testAndClearDirty(eventId, 1); 1766 1767 ContentValues attendee = new ContentValues(); 1768 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1769 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1770 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1771 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1772 Calendar.Attendees.RELATIONSHIP_ORGANIZER); 1773 attendee.put(Calendar.Attendees.EVENT_ID, eventId); 1774 1775 Uri attendeeUri = mResolver.insert( 1776 updatedUri(Calendar.Attendees.CONTENT_URI, syncAdapter), 1777 attendee); 1778 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1779 testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1); 1780 1781 ContentValues reminder = new ContentValues(); 1782 reminder.put(Calendar.Reminders.MINUTES, 10); 1783 reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_EMAIL); 1784 reminder.put(Calendar.Attendees.EVENT_ID, eventId); 1785 1786 Uri reminderUri = mResolver.insert( 1787 updatedUri(Calendar.Reminders.CONTENT_URI, syncAdapter), reminder); 1788 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1789 testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1); 1790 1791 ContentValues alert = new ContentValues(); 1792 alert.put(Calendar.CalendarAlerts.BEGIN, 10); 1793 alert.put(Calendar.CalendarAlerts.END, 20); 1794 alert.put(Calendar.CalendarAlerts.ALARM_TIME, 30); 1795 alert.put(Calendar.CalendarAlerts.CREATION_TIME, 40); 1796 alert.put(Calendar.CalendarAlerts.RECEIVED_TIME, 50); 1797 alert.put(Calendar.CalendarAlerts.NOTIFY_TIME, 60); 1798 alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.SCHEDULED); 1799 alert.put(Calendar.CalendarAlerts.MINUTES, 30); 1800 alert.put(Calendar.CalendarAlerts.EVENT_ID, eventId); 1801 1802 Uri alertUri = mResolver.insert( 1803 updatedUri(Calendar.CalendarAlerts.CONTENT_URI, syncAdapter), alert); 1804 // Alerts don't dirty the event 1805 testAndClearDirty(eventId, 0); 1806 testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); 1807 1808 ContentValues extended = new ContentValues(); 1809 extended.put(Calendar.ExtendedProperties.NAME, "foo"); 1810 extended.put(Calendar.ExtendedProperties.VALUE, "bar"); 1811 extended.put(Calendar.ExtendedProperties.EVENT_ID, eventId); 1812 1813 Uri extendedUri = mResolver.insert( 1814 updatedUri(Calendar.ExtendedProperties.CONTENT_URI, syncAdapter), extended); 1815 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1816 testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1); 1817 1818 // Now test updates 1819 1820 attendee = new ContentValues(); 1821 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Sam"); 1822 // Need to include EVENT_ID with attendee update. Is that desired? 1823 attendee.put(Calendar.Attendees.EVENT_ID, eventId); 1824 1825 assertEquals("update", 1, mResolver.update(updatedUri(attendeeUri, syncAdapter), attendee, 1826 null /* where */, null /* selectionArgs */)); 1827 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1828 1829 testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1); 1830 1831 reminder = new ContentValues(); 1832 reminder.put(Calendar.Reminders.MINUTES, 20); 1833 1834 assertEquals("update", 1, mResolver.update(updatedUri(reminderUri, syncAdapter), reminder, 1835 null /* where */, null /* selectionArgs */)); 1836 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1837 testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1); 1838 1839 alert = new ContentValues(); 1840 alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.DISMISSED); 1841 1842 assertEquals("update", 1, mResolver.update(updatedUri(alertUri, syncAdapter), alert, 1843 null /* where */, null /* selectionArgs */)); 1844 // Alerts don't dirty the event 1845 testAndClearDirty(eventId, 0); 1846 testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); 1847 1848 extended = new ContentValues(); 1849 extended.put(Calendar.ExtendedProperties.VALUE, "baz"); 1850 1851 assertEquals("update", 1, mResolver.update(updatedUri(extendedUri, syncAdapter), extended, 1852 null /* where */, null /* selectionArgs */)); 1853 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1854 testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1); 1855 1856 // Now test deletes 1857 1858 assertEquals("delete", 1, mResolver.delete( 1859 updatedUri(attendeeUri, syncAdapter), 1860 null, null /* selectionArgs */)); 1861 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1862 testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 0); 1863 1864 assertEquals("delete", 1, mResolver.delete(updatedUri(reminderUri, syncAdapter), 1865 null /* where */, null /* selectionArgs */)); 1866 1867 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1868 testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 0); 1869 1870 assertEquals("delete", 1, mResolver.delete(updatedUri(alertUri, syncAdapter), 1871 null /* where */, null /* selectionArgs */)); 1872 1873 // Alerts don't dirty the event 1874 testAndClearDirty(eventId, 0); 1875 testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0); 1876 1877 assertEquals("delete", 1, mResolver.delete(updatedUri(extendedUri, syncAdapter), 1878 null /* where */, null /* selectionArgs */)); 1879 1880 testAndClearDirty(eventId, syncAdapter ? 0 : 1); 1881 testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 0); 1882 } 1883 1884 /** 1885 * Test calendar deletion 1886 * @throws Exception 1887 */ 1888 public void testCalendarDeletion() throws Exception { 1889 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1890 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 1891 long eventId = ContentUris.parseId(eventUri); 1892 testAndClearDirty(eventId, 1); 1893 Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1")); 1894 long eventId1 = ContentUris.parseId(eventUri); 1895 assertEquals("delete", 1, mResolver.delete(eventUri1, null, null)); 1896 // Calendar has one event and one deleted event 1897 testQueryCount(Calendar.Events.CONTENT_URI, null, 2); 1898 1899 assertEquals("delete", 1, mResolver.delete(Calendar.Calendars.CONTENT_URI, 1900 "_id=" + mCalendarId, null)); 1901 // Calendar should be deleted 1902 testQueryCount(Calendar.Calendars.CONTENT_URI, null, 0); 1903 // Event should be gone 1904 testQueryCount(Calendar.Events.CONTENT_URI, null, 0); 1905 } 1906 1907 /** 1908 * Test multiple account support. 1909 */ 1910 public void testMultipleAccounts() throws Exception { 1911 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1912 int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com"); 1913 Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0")); 1914 Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1")); 1915 1916 testQueryCount(Calendar.Events.CONTENT_URI, null, 2); 1917 Uri eventsWithAccount = Calendar.Events.CONTENT_URI.buildUpon() 1918 .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_NAME, "joe@joe.com") 1919 .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_TYPE, "com.google") 1920 .build(); 1921 // Only one event for that account 1922 testQueryCount(eventsWithAccount, null, 1); 1923 1924 // Test deletion with account and selection 1925 1926 long eventId = ContentUris.parseId(eventUri1); 1927 // Wrong account, should not be deleted 1928 assertEquals("delete", 0, mResolver.delete( 1929 updatedUri(eventsWithAccount, true /* syncAdapter */), 1930 "_id=" + eventId, null /* selectionArgs */)); 1931 testQueryCount(Calendar.Events.CONTENT_URI, null, 2); 1932 // Right account, should be deleted 1933 assertEquals("delete", 1, mResolver.delete( 1934 updatedUri(Calendar.Events.CONTENT_URI, true /* syncAdapter */), 1935 "_id=" + eventId, null /* selectionArgs */)); 1936 testQueryCount(Calendar.Events.CONTENT_URI, null, 1); 1937 } 1938 1939 /** 1940 * Run commands, wiping instance table at each step. 1941 * This tests full instance expansion. 1942 * @throws Exception 1943 */ 1944 public void testCommandSequences1() throws Exception { 1945 commandSequences(true); 1946 } 1947 1948 /** 1949 * Run commands normally. 1950 * This tests incremental instance expansion. 1951 * @throws Exception 1952 */ 1953 public void testCommandSequences2() throws Exception { 1954 commandSequences(false); 1955 } 1956 1957 /** 1958 * Run thorough set of command sequences 1959 * @param wipe true if instances should be wiped and regenerated 1960 * @throws Exception 1961 */ 1962 private void commandSequences(boolean wipe) throws Exception { 1963 Cursor cursor; 1964 Uri url = null; 1965 mWipe = wipe; // Set global flag 1966 1967 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1968 1969 cursor = mResolver.query(mEventsUri, null, null, null, null); 1970 assertEquals(0, cursor.getCount()); 1971 cursor.close(); 1972 Command[] commands; 1973 1974 Log.i(TAG, "Normal insert/delete"); 1975 commands = mNormalInsertDelete; 1976 for (Command command : commands) { 1977 command.execute(); 1978 } 1979 1980 deleteAllEvents(); 1981 1982 Log.i(TAG, "All-day insert/delete"); 1983 commands = mAlldayInsertDelete; 1984 for (Command command : commands) { 1985 command.execute(); 1986 } 1987 1988 deleteAllEvents(); 1989 1990 Log.i(TAG, "Recurring insert/delete"); 1991 commands = mRecurringInsertDelete; 1992 for (Command command : commands) { 1993 command.execute(); 1994 } 1995 1996 deleteAllEvents(); 1997 1998 Log.i(TAG, "Exception with truncated recurrence"); 1999 commands = mExceptionWithTruncatedRecurrence; 2000 for (Command command : commands) { 2001 command.execute(); 2002 } 2003 2004 deleteAllEvents(); 2005 2006 Log.i(TAG, "Exception with moved recurrence"); 2007 commands = mExceptionWithMovedRecurrence; 2008 for (Command command : commands) { 2009 command.execute(); 2010 } 2011 2012 deleteAllEvents(); 2013 2014 Log.i(TAG, "Exception with cancel"); 2015 commands = mCancelInstance; 2016 for (Command command : commands) { 2017 command.execute(); 2018 } 2019 2020 deleteAllEvents(); 2021 2022 Log.i(TAG, "Exception with moved recurrence2"); 2023 commands = mExceptionWithMovedRecurrence2; 2024 for (Command command : commands) { 2025 command.execute(); 2026 } 2027 2028 deleteAllEvents(); 2029 2030 Log.i(TAG, "Exception with no recurrence"); 2031 commands = mExceptionWithNoRecurrence; 2032 for (Command command : commands) { 2033 command.execute(); 2034 } 2035 } 2036 2037 /** 2038 * Test Time toString. 2039 * @throws Exception 2040 */ 2041 // Suppressed because toString currently hangs. 2042 @Suppress 2043 public void testTimeToString() throws Exception { 2044 Time time = new Time(Time.TIMEZONE_UTC); 2045 String str = "2039-01-01T23:00:00.000Z"; 2046 String result = "20390101T230000UTC(0,0,0,-1,0)"; 2047 time.parse3339(str); 2048 assertEquals(result, time.toString()); 2049 } 2050 2051 /** 2052 * Test the query done by Event.loadEvents 2053 * Also test that instance queries work when an even straddles the expansion range 2054 * @throws Exception 2055 */ 2056 public void testInstanceQuery() throws Exception { 2057 final String[] PROJECTION = new String[] { 2058 Instances.TITLE, // 0 2059 Instances.EVENT_LOCATION, // 1 2060 Instances.ALL_DAY, // 2 2061 Instances.COLOR, // 3 2062 Instances.EVENT_TIMEZONE, // 4 2063 Instances.EVENT_ID, // 5 2064 Instances.BEGIN, // 6 2065 Instances.END, // 7 2066 Instances._ID, // 8 2067 Instances.START_DAY, // 9 2068 Instances.END_DAY, // 10 2069 Instances.START_MINUTE, // 11 2070 Instances.END_MINUTE, // 12 2071 Instances.HAS_ALARM, // 13 2072 Instances.RRULE, // 14 2073 Instances.RDATE, // 15 2074 Instances.SELF_ATTENDEE_STATUS, // 16 2075 Events.ORGANIZER, // 17 2076 Events.GUESTS_CAN_MODIFY, // 18 2077 }; 2078 2079 String orderBy = Instances.SORT_CALENDAR_VIEW; 2080 String where = Instances.SELF_ATTENDEE_STATUS + "!=" + Calendar.Attendees.ATTENDEE_STATUS_DECLINED; 2081 2082 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2083 final String START = "2008-05-01T00:00:00"; 2084 final String END = "2008-05-01T20:00:00"; 2085 2086 EventInfo[] events = { new EventInfo("normal0", 2087 START, 2088 END, 2089 false /* allDay */, 2090 DEFAULT_TIMEZONE) }; 2091 2092 insertEvent(calId, events[0]); 2093 2094 Time time = new Time(DEFAULT_TIMEZONE); 2095 time.parse3339(START); 2096 long startMs = time.toMillis(true /* ignoreDst */); 2097 // Query starting from way in the past to one hour into the event. 2098 // Query is more than 2 months so the range won't get extended by the provider. 2099 Cursor cursor = Instances.query(mResolver, PROJECTION, 2100 startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS, 2101 where, orderBy); 2102 try { 2103 assertEquals(1, cursor.getCount()); 2104 } finally { 2105 cursor.close(); 2106 } 2107 2108 // Now expand the instance range. The event overlaps the new part of the range. 2109 cursor = Instances.query(mResolver, PROJECTION, 2110 startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS, 2111 where, orderBy); 2112 try { 2113 assertEquals(1, cursor.getCount()); 2114 } finally { 2115 cursor.close(); 2116 } 2117 } 2118 2119 private Cursor queryInstances(long begin, long end) { 2120 Uri url = Uri.withAppendedPath(Calendar.Instances.CONTENT_URI, begin + "/" + end); 2121 return mResolver.query(url, null, null, null, null); 2122 } 2123 2124 protected static class MockProvider extends ContentProvider { 2125 2126 private String mAuthority; 2127 2128 private int mNumItems = 0; 2129 2130 public MockProvider(String authority) { 2131 mAuthority = authority; 2132 } 2133 2134 @Override 2135 public boolean onCreate() { 2136 return true; 2137 } 2138 2139 @Override 2140 public Cursor query(Uri uri, String[] projection, String selection, 2141 String[] selectionArgs, String sortOrder) { 2142 return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>()); 2143 } 2144 2145 @Override 2146 public String getType(Uri uri) { 2147 throw new UnsupportedOperationException(); 2148 } 2149 2150 @Override 2151 public Uri insert(Uri uri, ContentValues values) { 2152 mNumItems++; 2153 return Uri.parse("content://" + mAuthority + "/" + mNumItems); 2154 } 2155 2156 @Override 2157 public int delete(Uri uri, String selection, String[] selectionArgs) { 2158 return 0; 2159 } 2160 2161 @Override 2162 public int update(Uri uri, ContentValues values, String selection, 2163 String[] selectionArgs) { 2164 return 0; 2165 } 2166 } 2167 2168 private void cleanCalendarDataTable(SQLiteOpenHelper helper) { 2169 if (null == helper) { 2170 return; 2171 } 2172 SQLiteDatabase db = helper.getWritableDatabase(); 2173 db.execSQL("DELETE FROM CalendarCache;"); 2174 } 2175 2176 public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException { 2177 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 2178 cleanCalendarDataTable(helper); 2179 CalendarCache cache = new CalendarCache(helper); 2180 2181 boolean hasException = false; 2182 try { 2183 String value = cache.readData(null); 2184 } catch (CalendarCache.CacheException e) { 2185 hasException = true; 2186 } 2187 assertTrue(hasException); 2188 2189 assertNull(cache.readTimezoneDatabaseVersion()); 2190 2191 cache.writeTimezoneDatabaseVersion("1234"); 2192 assertEquals("1234", cache.readTimezoneDatabaseVersion()); 2193 2194 cache.writeTimezoneDatabaseVersion("5678"); 2195 assertEquals("5678", cache.readTimezoneDatabaseVersion()); 2196 } 2197 2198 private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) { 2199 Uri uri = Uri.parse("content://" + Calendar.AUTHORITY + "/events"); 2200 Log.i(TAG, "Looking for EventId = " + eventId); 2201 2202 Cursor cursor = mResolver.query(uri, null, null, null, null); 2203 assertEquals(1, cursor.getCount()); 2204 2205 int colIndexTitle = cursor.getColumnIndex(Calendar.Events.TITLE); 2206 int colIndexDtStart = cursor.getColumnIndex(Calendar.Events.DTSTART); 2207 int colIndexDtEnd = cursor.getColumnIndex(Calendar.Events.DTEND); 2208 int colIndexAllDay = cursor.getColumnIndex(Calendar.Events.ALL_DAY); 2209 if (!cursor.moveToNext()) { 2210 Log.e(TAG,"Could not find inserted event"); 2211 assertTrue(false); 2212 } 2213 assertEquals(title, cursor.getString(colIndexTitle)); 2214 assertEquals(dtStart, cursor.getLong(colIndexDtStart)); 2215 assertEquals(dtEnd, cursor.getLong(colIndexDtEnd)); 2216 assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0)); 2217 cursor.close(); 2218 } 2219 2220 public void testChangeTimezoneDB() { 2221 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 2222 2223 Cursor cursor = mResolver.query(Calendar.Events.CONTENT_URI, null, null, null, null); 2224 assertEquals(0, cursor.getCount()); 2225 cursor.close(); 2226 2227 EventInfo[] events = { new EventInfo("normal0", 2228 "2008-05-01T00:00:00", 2229 "2008-05-02T00:00:00", 2230 false, 2231 DEFAULT_TIMEZONE) }; 2232 2233 Uri uri = insertEvent(calId, events[0]); 2234 assertNotNull(uri); 2235 2236 // check the inserted event 2237 checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay); 2238 2239// TODO (fdimeglio): uncomment when the VM is more stable 2240// // check timezone database version 2241// assertEquals(TimeUtils.getTimeZoneDatabaseVersion(), 2242// getProvider().getTimezoneDatabaseVersion()); 2243 2244 // inject a new time zone 2245 getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE, 2246 MOCK_TIME_ZONE_DATABASE_VERSION); 2247 2248 // check timezone database version 2249 assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion()); 2250 2251 // check if the inserted event as been updated with the timezone information 2252 // there is 1h time difference between America/LosAngeles and America/Anchorage 2253 long deltaMillisForTimezones = 3600000L; 2254 checkEvent(1, events[0].mTitle, 2255 events[0].mDtstart + deltaMillisForTimezones, 2256 events[0].mDtend + deltaMillisForTimezones, 2257 events[0].mAllDay); 2258 } 2259} 2260