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