CalendarProvider2Test.java revision 9f005e4843925efe4fa8434361c4ad4ad384ed4c
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.providers.calendar; 18 19import com.android.internal.database.ArrayListCursor; 20 21import android.content.*; 22import android.database.Cursor; 23import android.database.sqlite.SQLiteDatabase; 24import android.net.Uri; 25import android.text.format.DateUtils; 26import android.text.format.Time; 27import android.provider.Calendar; 28import android.provider.Calendar.BusyBits; 29import android.provider.Calendar.Calendars; 30import android.provider.Calendar.Events; 31import android.provider.Calendar.Instances; 32import android.test.ProviderTestCase2; 33import android.test.mock.MockContentResolver; 34import android.test.suitebuilder.annotation.LargeTest; 35import android.test.suitebuilder.annotation.Suppress; 36import android.util.Log; 37 38import java.util.ArrayList; 39 40/** 41 * Runs various tests on an isolated Calendar provider with its own database. 42 */ 43@LargeTest 44public class CalendarProvider2Test extends ProviderTestCase2<CalendarProvider2ForTesting> { 45 static final String TAG = "calendar"; 46 static final String DEFAULT_TIMEZONE = "America/Los_Angeles"; 47 48 private SQLiteDatabase mDb; 49 private MetaData mMetaData; 50 private Context mContext; 51 private MockContentResolver mResolver; 52 private Uri mEventsUri = Uri.parse("content://calendar/events"); 53 private int mCalendarId; 54 55 protected boolean mWipe = false; 56 57 // We need a unique id to put in the _sync_id field so that we can create 58 // recurrence exceptions that refer to recurring events. 59 private int mGlobalSyncId = 1000; 60 private static final String CALENDAR_URL = 61 "http://www.google.com/calendar/feeds/joe%40joe.com/private/full"; 62 63 /** 64 * KeyValue is a simple class that stores a pair of strings representing 65 * a (key, value) pair. This is used for updating events. 66 */ 67 private class KeyValue { 68 String key; 69 String value; 70 71 public KeyValue(String key, String value) { 72 this.key = key; 73 this.value = value; 74 } 75 } 76 77 /** 78 * A generic command interface. This is used to support a sequence of 79 * commands that can create events, delete or update events, and then 80 * check that the state of the database is as expected. 81 */ 82 private interface Command { 83 public void execute(); 84 } 85 86 /** 87 * This is used to insert a new event into the database. The event is 88 * specified by its name (or "title"). All of the event fields (the 89 * start and end time, whether it is an all-day event, and so on) are 90 * stored in a separate table (the "mEvents" table). 91 */ 92 private class Insert implements Command { 93 EventInfo eventInfo; 94 95 public Insert(String eventName) { 96 eventInfo = findEvent(eventName); 97 } 98 99 public void execute() { 100 Log.i(TAG, "insert " + eventInfo.mTitle); 101 insertEvent(mCalendarId, eventInfo); 102 } 103 } 104 105 /** 106 * This is used to delete an event, specified by the event name. 107 */ 108 private class Delete implements Command { 109 String eventName; 110 int expected; 111 112 public Delete(String eventName, int expected) { 113 this.eventName = eventName; 114 this.expected = expected; 115 } 116 117 public void execute() { 118 Log.i(TAG, "delete " + eventName); 119 int rows = deleteMatchingEvents(eventName); 120 assertEquals(expected, rows); 121 } 122 } 123 124 /** 125 * This is used to update an event. The values to update are specified 126 * with an array of (key, value) pairs. Both the key and value are 127 * specified as strings. Event fields that are not really strings (such 128 * as DTSTART which is a long) should be converted to the appropriate type 129 * but that isn't supported yet. When needed, that can be added here 130 * by checking for specific keys and converting the associated values. 131 */ 132 private class Update implements Command { 133 String eventName; 134 KeyValue[] pairs; 135 136 public Update(String eventName, KeyValue[] pairs) { 137 this.eventName = eventName; 138 this.pairs = pairs; 139 } 140 141 public void execute() { 142 Log.i(TAG, "update " + eventName); 143 if (mWipe) { 144 // Wipe instance table so it will be regenerated 145 mMetaData.clearInstanceRange(); 146 } 147 ContentValues map = new ContentValues(); 148 for (KeyValue pair : pairs) { 149 String value = pair.value; 150 if (Calendar.EventsColumns.STATUS.equals(pair.key)) { 151 // Do type conversion for STATUS 152 map.put(pair.key, Integer.parseInt(value)); 153 } else { 154 map.put(pair.key, value); 155 } 156 } 157 updateMatchingEvents(eventName, map); 158 } 159 } 160 161 /** 162 * This command queries the number of events and compares it to the given 163 * expected value. 164 */ 165 private class QueryNumEvents implements Command { 166 int expected; 167 168 public QueryNumEvents(int expected) { 169 this.expected = expected; 170 } 171 172 public void execute() { 173 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 174 assertEquals(expected, cursor.getCount()); 175 cursor.close(); 176 } 177 } 178 179 180 /** 181 * This command dumps the list of events to the log for debugging. 182 */ 183 private class DumpEvents implements Command { 184 185 public DumpEvents() { 186 } 187 188 public void execute() { 189 Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); 190 dumpCursor(cursor); 191 cursor.close(); 192 } 193 } 194 195 /** 196 * This command dumps the list of instances to the log for debugging. 197 */ 198 private class DumpInstances implements Command { 199 long begin; 200 long end; 201 202 public DumpInstances(String startDate, String endDate) { 203 Time time = new Time(DEFAULT_TIMEZONE); 204 time.parse3339(startDate); 205 begin = time.toMillis(false /* use isDst */); 206 time.parse3339(endDate); 207 end = time.toMillis(false /* use isDst */); 208 } 209 210 public void execute() { 211 Cursor cursor = queryInstances(begin, end); 212 dumpCursor(cursor); 213 cursor.close(); 214 } 215 } 216 217 /** 218 * This command queries the number of instances and compares it to the given 219 * expected value. 220 */ 221 private class QueryNumInstances implements Command { 222 int expected; 223 long begin; 224 long end; 225 226 public QueryNumInstances(String startDate, String endDate, int expected) { 227 Time time = new Time(DEFAULT_TIMEZONE); 228 time.parse3339(startDate); 229 begin = time.toMillis(false /* use isDst */); 230 time.parse3339(endDate); 231 end = time.toMillis(false /* use isDst */); 232 this.expected = expected; 233 } 234 235 public void execute() { 236 Cursor cursor = queryInstances(begin, end); 237 assertEquals(expected, cursor.getCount()); 238 cursor.close(); 239 } 240 } 241 242 /** 243 * When this command runs it verifies that all of the instances in the 244 * given range match the expected instances (each instance is specified by 245 * a start date). 246 * If you just want to verify that an instance exists in a given date 247 * range, use {@link VerifyInstance} instead. 248 */ 249 private class VerifyAllInstances implements Command { 250 long[] instances; 251 long begin; 252 long end; 253 254 public VerifyAllInstances(String startDate, String endDate, String[] dates) { 255 Time time = new Time(DEFAULT_TIMEZONE); 256 time.parse3339(startDate); 257 begin = time.toMillis(false /* use isDst */); 258 time.parse3339(endDate); 259 end = time.toMillis(false /* use isDst */); 260 261 if (dates == null) { 262 return; 263 } 264 265 // Convert all the instance date strings to UTC milliseconds 266 int len = dates.length; 267 this.instances = new long[len]; 268 int index = 0; 269 for (String instance : dates) { 270 time.parse3339(instance); 271 this.instances[index++] = time.toMillis(false /* use isDst */); 272 } 273 } 274 275 public void execute() { 276 Cursor cursor = queryInstances(begin, end); 277 int len = 0; 278 if (instances != null) { 279 len = instances.length; 280 } 281 if (len != cursor.getCount()) { 282 dumpCursor(cursor); 283 } 284 assertEquals("number of instances don't match", len, cursor.getCount()); 285 286 if (instances == null) { 287 return; 288 } 289 290 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 291 while (cursor.moveToNext()) { 292 long begin = cursor.getLong(beginColumn); 293 294 // Search the list of expected instances for a matching start 295 // time. 296 boolean found = false; 297 for (long instance : instances) { 298 if (instance == begin) { 299 found = true; 300 break; 301 } 302 } 303 if (!found) { 304 int titleColumn = cursor.getColumnIndex(Events.TITLE); 305 int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY); 306 307 String title = cursor.getString(titleColumn); 308 boolean allDay = cursor.getInt(allDayColumn) != 0; 309 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | 310 DateUtils.FORMAT_24HOUR; 311 if (allDay) { 312 flags |= DateUtils.FORMAT_UTC; 313 } else { 314 flags |= DateUtils.FORMAT_SHOW_TIME; 315 } 316 String date = DateUtils.formatDateRange(mContext, begin, begin, flags); 317 String mesg = String.format("Test failed!" 318 + " unexpected instance (\"%s\") at %s", 319 title, date); 320 Log.e(TAG, mesg); 321 } 322 if (!found) { 323 dumpCursor(cursor); 324 } 325 assertTrue(found); 326 } 327 cursor.close(); 328 } 329 } 330 331 /** 332 * When this command runs it verifies that the given instance exists in 333 * the given date range. 334 */ 335 private class VerifyInstance implements Command { 336 long instance; 337 boolean allDay; 338 long begin; 339 long end; 340 341 /** 342 * Creates a command to check that the given range [startDate,endDate] 343 * contains a specific instance of an event (specified by "date"). 344 * 345 * @param startDate the beginning of the date range 346 * @param endDate the end of the date range 347 * @param date the date or date-time string of an event instance 348 */ 349 public VerifyInstance(String startDate, String endDate, String date) { 350 Time time = new Time(DEFAULT_TIMEZONE); 351 time.parse3339(startDate); 352 begin = time.toMillis(false /* use isDst */); 353 time.parse3339(endDate); 354 end = time.toMillis(false /* use isDst */); 355 356 // Convert the instance date string to UTC milliseconds 357 time.parse3339(date); 358 allDay = time.allDay; 359 instance = time.toMillis(false /* use isDst */); 360 } 361 362 public void execute() { 363 Cursor cursor = queryInstances(begin, end); 364 int beginColumn = cursor.getColumnIndex(Instances.BEGIN); 365 boolean found = false; 366 while (cursor.moveToNext()) { 367 long begin = cursor.getLong(beginColumn); 368 369 if (instance == begin) { 370 found = true; 371 break; 372 } 373 } 374 if (!found) { 375 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE; 376 if (allDay) { 377 flags |= DateUtils.FORMAT_UTC; 378 } else { 379 flags |= DateUtils.FORMAT_SHOW_TIME; 380 } 381 String date = DateUtils.formatDateRange(mContext, instance, instance, flags); 382 String mesg = String.format("Test failed!" 383 + " cannot find instance at %s", 384 date); 385 Log.e(TAG, mesg); 386 } 387 assertTrue(found); 388 cursor.close(); 389 } 390 } 391 392 /** 393 * This class stores all the useful information about an event. 394 */ 395 private class EventInfo { 396 String mTitle; 397 String mDescription; 398 String mTimezone; 399 boolean mAllDay; 400 long mDtstart; 401 long mDtend; 402 String mRrule; 403 String mDuration; 404 String mOriginalTitle; 405 long mOriginalInstance; 406 int mSyncId; 407 408 // Constructor for normal events, using the default timezone 409 public EventInfo(String title, String startDate, String endDate, 410 boolean allDay) { 411 init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE); 412 } 413 414 // Constructor for normal events, specifying the timezone 415 public EventInfo(String title, String startDate, String endDate, 416 boolean allDay, String timezone) { 417 init(title, startDate, endDate, allDay, timezone); 418 } 419 420 public void init(String title, String startDate, String endDate, 421 boolean allDay, String timezone) { 422 mTitle = title; 423 Time time = new Time(); 424 if (allDay) { 425 time.timezone = Time.TIMEZONE_UTC; 426 } else if (timezone != null) { 427 time.timezone = timezone; 428 } 429 mTimezone = time.timezone; 430 time.parse3339(startDate); 431 mDtstart = time.toMillis(false /* use isDst */); 432 time.parse3339(endDate); 433 mDtend = time.toMillis(false /* use isDst */); 434 mDuration = null; 435 mRrule = null; 436 mAllDay = allDay; 437 } 438 439 // Constructor for repeating events, using the default timezone 440 public EventInfo(String title, String description, String startDate, String endDate, 441 String rrule, boolean allDay) { 442 init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE); 443 } 444 445 // Constructor for repeating events, specifying the timezone 446 public EventInfo(String title, String description, String startDate, String endDate, 447 String rrule, boolean allDay, String timezone) { 448 init(title, description, startDate, endDate, rrule, allDay, timezone); 449 } 450 451 public void init(String title, String description, String startDate, String endDate, 452 String rrule, boolean allDay, String timezone) { 453 mTitle = title; 454 mDescription = description; 455 Time time = new Time(); 456 if (allDay) { 457 time.timezone = Time.TIMEZONE_UTC; 458 } else if (timezone != null) { 459 time.timezone = timezone; 460 } 461 mTimezone = time.timezone; 462 time.parse3339(startDate); 463 mDtstart = time.toMillis(false /* use isDst */); 464 if (endDate != null) { 465 time.parse3339(endDate); 466 mDtend = time.toMillis(false /* use isDst */); 467 } 468 if (allDay) { 469 long days = 1; 470 if (endDate != null) { 471 days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS; 472 } 473 mDuration = "P" + days + "D"; 474 } else { 475 long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS; 476 mDuration = "P" + seconds + "S"; 477 } 478 mRrule = rrule; 479 mAllDay = allDay; 480 } 481 482 // Constructor for recurrence exceptions, using the default timezone 483 public EventInfo(String originalTitle, String originalInstance, String title, 484 String description, String startDate, String endDate, boolean allDay) { 485 init(originalTitle, originalInstance, 486 title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE); 487 } 488 489 public void init(String originalTitle, String originalInstance, 490 String title, String description, String startDate, String endDate, 491 boolean allDay, String timezone) { 492 mOriginalTitle = originalTitle; 493 Time time = new Time(timezone); 494 time.parse3339(originalInstance); 495 mOriginalInstance = time.toMillis(false /* use isDst */); 496 init(title, description, startDate, endDate, null /* rrule */, allDay, timezone); 497 } 498 } 499 500 private class InstanceInfo { 501 EventInfo mEvent; 502 long mBegin; 503 long mEnd; 504 int mExpectedOccurrences; 505 506 public InstanceInfo(String eventName, String startDate, String endDate, int expected) { 507 // Find the test index that contains the given event name 508 mEvent = findEvent(eventName); 509 Time time = new Time(mEvent.mTimezone); 510 time.parse3339(startDate); 511 mBegin = time.toMillis(false /* use isDst */); 512 time.parse3339(endDate); 513 mEnd = time.toMillis(false /* use isDst */); 514 mExpectedOccurrences = expected; 515 } 516 } 517 518 private class BusyBitInfo { 519 EventInfo[] mEvents; 520 int mStartDay; 521 int mNumDays; 522 int[] mBusyBits; 523 int[] mAllDayCounts; 524 525 public BusyBitInfo(EventInfo[] events, String startDate, int numDays, 526 int[] busybits, int[] allDayCounts) { 527 mEvents = events; 528 Time time = new Time(DEFAULT_TIMEZONE); 529 time.parse3339(startDate); 530 long millis = time.toMillis(true /* ignore isDst */); 531 mStartDay = Time.getJulianDay(millis, time.gmtoff); 532 mNumDays = numDays; 533 mBusyBits = busybits; 534 mAllDayCounts = allDayCounts; 535 } 536 } 537 538 /** 539 * This is the main table of events. The events in this table are 540 * referred to by name in other places. 541 */ 542 private EventInfo[] mEvents = { 543 new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false), 544 new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false), 545 new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false), 546 new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true), 547 new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true), 548 new EventInfo("daily0", "daily from 5/1/2008 12am to 1am", 549 "2008-05-01T00:00:00", "2008-05-01T01:00:00", 550 "FREQ=DAILY;WKST=SU", false), 551 new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am", 552 "2008-05-01T08:30:00", "2008-05-01T09:30:00", 553 "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false), 554 new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am", 555 "2008-05-01T08:45:00", "2008-05-01T09:15:00", 556 "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false), 557 new EventInfo("allday daily0", "all-day daily from 5/1/2008", 558 "2008-05-01", null, 559 "FREQ=DAILY;WKST=SU", true), 560 new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008", 561 "2008-05-01", null, 562 "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true), 563 new EventInfo("allday weekly0", "all-day weekly from 5/1/2008", 564 "2008-05-01", null, 565 "FREQ=WEEKLY;WKST=SU", true), 566 new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008", 567 "2008-05-01", "2008-05-03", 568 "FREQ=WEEKLY;WKST=SU", true), 569 new EventInfo("allday yearly0", "all-day yearly on 5/1/2008", 570 "2008-05-01T", null, 571 "FREQ=YEARLY;WKST=SU", true), 572 new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm", 573 "2008-05-06T13:00:00", "2008-05-06T14:00:00", 574 "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false), 575 new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm", 576 "2008-05-06T14:30:00", "2008-05-06T15:30:00", 577 "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false), 578 new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm", 579 "2008-05-20T15:00:00", "2008-05-20T16:00:00", 580 "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false), 581 new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am", 582 "2008-05-01T00:00:00", "2008-05-01T00:10:00", 583 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false), 584 new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight", 585 "2008-05-31T23:00:00", "2008-06-01T00:00:00", 586 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false), 587 new EventInfo("daily0", "2008-05-01T00:00:00", 588 "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am", 589 "2008-05-01T02:00:00", "2008-05-01T01:03:00", false), 590 new EventInfo("daily0", "2008-05-03T00:00:00", 591 "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am", 592 "2008-05-03T02:00:00", "2008-05-03T01:03:00", false), 593 new EventInfo("daily0", "2008-05-02T00:00:00", 594 "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008", 595 "2008-01-02T00:00:00", "2008-01-02T01:00:00", false), 596 new EventInfo("weekly0", "2008-05-13T13:00:00", 597 "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm", 598 "2008-12-11T13:00:00", "2008-12-11T14:00:00", false), 599 new EventInfo("weekly0", "2008-05-13T13:00:00", 600 "cancel0", "weekly0 exception for 5/13/2008 1pm", 601 "2008-05-13T13:00:00", "2008-05-13T14:00:00", false), 602 new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm", 603 "2008-05-01T13:00:00", "2008-05-01T14:00:00", 604 "FREQ=YEARLY;WKST=SU", false), 605 }; 606 607 /** 608 * This table is used to create repeating events and then check that the 609 * number of instances within a given range matches the expected number 610 * of instances. 611 */ 612 private InstanceInfo[] mInstanceRanges = { 613 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1), 614 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 615 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2), 616 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2), 617 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 618 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1), 619 new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2), 620 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31), 621 new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32), 622 623 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 624 new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2), 625 626 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), 627 new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3), 628 629 new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7), 630 new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3), 631 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1), 632 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2), 633 new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5), 634 new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5), 635 new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1), 636 new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2), 637 638 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 639 new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 640 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4), 641 new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8), 642 643 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), 644 new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), 645 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2), 646 new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4), 647 648 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0), 649 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1), 650 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0), 651 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0), 652 new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1), 653 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 654 new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2), 655 656 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), 657 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), 658 new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1), 659 new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0), 660 new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2), 661 662 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0), 663 new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1), 664 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1), 665 new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2), 666 667 new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1), 668 new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2), 669 }; 670 671 /** 672 * This tables of events is used to test the BusyBit database table. 673 */ 674 private EventInfo[] mBusyBitEvents = { 675 new EventInfo("1: 12am - 1am", "2008-05-01T00:00:00", "2008-05-01T01:00:00", false), 676 new EventInfo("2: 1:30am - 2am", "2008-05-02T01:30:00", "2008-05-02T02:00:00", false), 677 new EventInfo("3: 3am - 5am", "2008-05-03T03:00:00", "2008-05-03T05:00:00", false), 678 new EventInfo("4: 12am - 5am", "2008-05-04T00:00:00", "2008-05-04T05:00:00", false), 679 new EventInfo("5: 1am - 2am", "2008-05-05T01:00:00", "2008-05-05T02:00:00", false), 680 new EventInfo("5: 8am - 9am", "2008-05-05T08:00:00", "2008-05-05T09:00:00", false), 681 new EventInfo("6: 1am - 10am", "2008-05-06T01:00:00", "2008-05-06T10:00:00", false), 682 new EventInfo("6: 8am - 9am", "2008-05-06T08:00:00", "2008-05-06T09:00:00", false), 683 new EventInfo("7: 1am - 5am", "2008-05-07T01:00:00", "2008-05-07T05:00:00", false), 684 new EventInfo("7: 12am - 2am", "2008-05-07T00:00:00", "2008-05-07T02:00:00", false), 685 new EventInfo("7: 8am - 9am", "2008-05-07T08:00:00", "2008-05-07T09:00:00", false), 686 new EventInfo("7: 1pm - 2pm", "2008-05-07T13:00:00", "2008-05-07T14:00:00", false), 687 new EventInfo("7: 3:30pm - 4:30pm", "2008-05-07T15:30:00", "2008-05-07T16:30:00", 688 false), 689 new EventInfo("7: 7pm - 8pm", "2008-05-07T19:00:00", "2008-05-07T20:00:00", false), 690 new EventInfo("7: 6:30pm - 7:30pm", "2008-05-07T18:30:00", "2008-05-07T19:30:00", 691 false), 692 new EventInfo("7: 11pm - midnight", "2008-05-07T23:00:00", "2008-05-08T00:00:00", 693 false), 694 new EventInfo("8: 1am - 2am", "2008-05-08T01:00:00", "2008-05-08T02:00:00", false), 695 new EventInfo("8: 3am - 4am", "2008-05-08T03:00:00", "2008-05-08T04:00:00", false), 696 new EventInfo("8: 5am - 6am", "2008-05-08T05:00:00", "2008-05-08T06:00:00", false), 697 new EventInfo("8: 7am - 8am", "2008-05-08T07:00:00", "2008-05-08T08:00:00", false), 698 new EventInfo("8: 9am - 10am", "2008-05-08T09:00:00", "2008-05-08T10:00:00", false), 699 new EventInfo("8: 11am - 12pm", "2008-05-08T11:00:00", "2008-05-08T12:00:00", false), 700 new EventInfo("8: 1pm - 2pm", "2008-05-08T13:00:00", "2008-05-08T14:00:00", false), 701 new EventInfo("8: 3pm - 4pm", "2008-05-08T15:00:00", "2008-05-08T16:00:00", false), 702 new EventInfo("8: 5pm - 6pm", "2008-05-08T17:00:00", "2008-05-08T18:00:00", false), 703 new EventInfo("8: 7pm - 8pm", "2008-05-08T19:00:00", "2008-05-08T20:00:00", false), 704 new EventInfo("8: 9pm - 10pm", "2008-05-08T21:00:00", "2008-05-08T22:00:00", false), 705 new EventInfo("8: 11pm - midnight", "2008-05-08T23:00:00", "2008-05-09T00:00:00", 706 false), 707 new EventInfo("10: 12am - midnight", "2008-05-10T00:00:00", "2008-05-11T00:00:00", 708 false), 709 new EventInfo("12: 1 day", "2008-05-12T00:00:00", "2008-05-13T00:00:00", true), 710 new EventInfo("14: 1 day", "2008-05-14T00:00:00", "2008-05-15T00:00:00", true), 711 new EventInfo("14: 2 days", "2008-05-14T00:00:00", "2008-05-16T00:00:00", true), 712 new EventInfo("14: 3 days", "2008-05-14T00:00:00", "2008-05-17T00:00:00", true), 713 new EventInfo("15: 1am - 2am", "2008-05-15T01:00:00", "2008-05-15T02:00:00", false), 714 new EventInfo("16: 10am - 11am", "2008-05-16T10:00:00", "2008-05-16T11:00:00", false), 715 new EventInfo("16: 11pm - midnight", "2008-05-16T23:00:00", "2008-05-17T00:00:00", 716 false), 717 }; 718 719 private EventInfo[] mBusyBitRepeatingEvents = { 720 new EventInfo("daily0", "daily from 5/1/2008 12am to 1am", 721 "2008-05-01T00:00:00", "2008-05-01T01:00:00", 722 "FREQ=DAILY;WKST=SU", false), 723 new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am", 724 "2008-05-01T08:30:00", "2008-05-01T09:30:00", 725 "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false), 726 new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm", 727 "2008-05-06T13:00:00", "2008-05-06T14:00:00", 728 "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false), 729 new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 4:30am to 5:30am", 730 "2008-05-06T04:30:00", "2008-05-06T05:30:00", 731 "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false), 732 new EventInfo("weekly2", "weekly from 5/5/2008 on Mon 1 day", 733 "2008-05-05T00:00:00", "2008-05-06T00:00:00", 734 "FREQ=WEEKLY;BYDAY=MO;WKST=MO", true), 735 new EventInfo("weekly3", "weekly from 5/7/2008 on Wed 3 days", 736 "2008-05-07T00:00:00", "2008-05-10T00:00:00", 737 "FREQ=WEEKLY;BYDAY=WE;WKST=SU", true), 738 new EventInfo("weekly4", "weekly from 5/8/2008 on Thu 3 days", 739 "2008-05-08T00:00:00", "2008-05-11T00:00:00", 740 "FREQ=WEEKLY;BYDAY=TH;WKST=SU", true), 741 new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm", 742 "2008-05-20T15:00:00", "2008-05-20T16:00:00", 743 "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false), 744 new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 11:00am to 11:10am", 745 "2008-05-01T11:00:00", "2008-05-01T11:10:00", 746 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false), 747 new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight", 748 "2008-05-31T23:00:00", "2008-06-01T00:00:00", 749 "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false), 750 }; 751 752 private BusyBitInfo[] mBusyBitTests = { 753 new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 1, 754 new int[] { 0x1 }, new int[] { 0 } ), 755 new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 1, 756 new int[] { 0x2 }, new int[] { 0 } ), 757 new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 2, 758 new int[] { 0x2, 0x18 }, new int[] { 0, 0 } ), 759 new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 3, 760 new int[] { 0x1, 0x2, 0x18 }, new int[] { 0, 0, 0 } ), 761 new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 8, 762 new int[] { 0x1, 0x2, 0x18, 0x1f, 0x102, 0x3fe, 0x8da11f, 0xaaaaaa }, 763 new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } ), 764 new BusyBitInfo(mBusyBitEvents, "2008-05-10T00:00:00", 4, 765 new int[] { 0xffffff, 0x0, 0x0, 0x0 }, new int[] { 0, 0, 1, 0 } ), 766 new BusyBitInfo(mBusyBitEvents, "2008-05-14T00:00:00", 4, 767 new int[] { 0x0, 0x2, 0x800400, 0x0 }, new int[] { 3, 2, 1, 0 } ), 768 769 // Repeating events 770 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 3, 771 new int[] { 0xb01, 0x301, 0x1 }, new int[] { 0, 0, 0 } ), 772 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 10, 773 new int[] { 0xb01, 0x301, 0x1, 0x1, 0x1, 0x2031, 0x1, 0x1, 0x1, 0x1 }, 774 new int[] { 0, 0, 0, 0, 1, 0, 1, 2, 2, 1 } ), 775 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-18T00:00:00", 11, 776 new int[] { 0x1, 0x1, 0xa031, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2001, 0x1 }, 777 new int[] { 0, 1, 0, 1, 2, 2, 1, 0, 1, 0, 1 } ), 778 new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-30T00:00:00", 5, 779 new int[] { 0x1, 0x800001, 0x801, 0x1, 0x2031 }, 780 new int[] { 2, 1, 0, 1, 0 } ), 781 }; 782 783 /** 784 * This sequence of commands inserts and deletes some events. 785 */ 786 private Command[] mNormalInsertDelete = { 787 new Insert("normal0"), 788 new Insert("normal1"), 789 new Insert("normal2"), 790 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3), 791 new Delete("normal1", 1), 792 new QueryNumEvents(2), 793 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2), 794 new Delete("normal1", 0), 795 new Delete("normal2", 1), 796 new QueryNumEvents(1), 797 new Delete("normal0", 1), 798 new QueryNumEvents(0), 799 }; 800 801 /** 802 * This sequence of commands inserts and deletes some all-day events. 803 */ 804 private Command[] mAlldayInsertDelete = { 805 new Insert("allday0"), 806 new Insert("allday1"), 807 new QueryNumEvents(2), 808 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0), 809 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2), 810 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 811 new Delete("allday0", 1), 812 new QueryNumEvents(1), 813 new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), 814 new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), 815 new Delete("allday1", 1), 816 new QueryNumEvents(0), 817 }; 818 819 /** 820 * This sequence of commands inserts and deletes some repeating events. 821 */ 822 private Command[] mRecurringInsertDelete = { 823 new Insert("daily0"), 824 new Insert("daily1"), 825 new QueryNumEvents(2), 826 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3), 827 new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2), 828 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6), 829 new Delete("daily1", 1), 830 new QueryNumEvents(1), 831 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2), 832 new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4), 833 new Delete("daily0", 1), 834 new QueryNumEvents(0), 835 }; 836 837 /** 838 * This sequence of commands creates a recurring event with a recurrence 839 * exception that moves an event outside the expansion window. It checks that the 840 * recurrence exception does not occur in the Instances database table. 841 * Bug 1642665 842 */ 843 private Command[] mExceptionWithMovedRecurrence = { 844 new Insert("daily0"), 845 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 846 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 847 "2008-05-03T00:00:00", }), 848 new Insert("except2"), 849 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 850 new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}), 851 }; 852 853 /** 854 * This sequence of commands deletes (cancels) one instance of a recurrence. 855 */ 856 private Command[] mCancelInstance = { 857 new Insert("weekly0"), 858 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 859 new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00", 860 "2008-05-20T13:00:00", }), 861 new Insert("cancel0"), 862 new Update("cancel0", new KeyValue[] { 863 new KeyValue(Calendar.EventsColumns.STATUS, 864 "" + Calendar.EventsColumns.STATUS_CANCELED), 865 }), 866 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", 867 new String[] {"2008-05-06T13:00:00", 868 "2008-05-20T13:00:00", }), 869 }; 870 /** 871 * This sequence of commands creates a recurring event with a recurrence 872 * exception that moves an event from outside the expansion window into the 873 * expansion window. 874 */ 875 private Command[] mExceptionWithMovedRecurrence2 = { 876 new Insert("weekly0"), 877 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 878 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 879 "2008-12-16T13:00:00", }), 880 new Insert("except3"), 881 new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", 882 new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", 883 "2008-12-11T13:00:00", "2008-12-16T13:00:00", }), 884 }; 885 /** 886 * This sequence of commands creates a recurring event with a recurrence 887 * exception and then changes the end time of the recurring event. It then 888 * checks that the recurrence exception does not occur in the Instances 889 * database table. 890 */ 891 private Command[] 892 mExceptionWithTruncatedRecurrence = { 893 new Insert("daily0"), 894 // Verify 4 occurrences of the "daily0" repeating event 895 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 896 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 897 "2008-05-03T00:00:00", "2008-05-04T00:00:00"}), 898 new Insert("except1"), 899 new QueryNumEvents(2), 900 901 // Verify that one of the 4 occurrences has its start time changed 902 // so that it now matches the recurrence exception. 903 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 904 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 905 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 906 907 // Change the end time of "daily0" but it still includes the 908 // recurrence exception. 909 new Update("daily0", new KeyValue[] { 910 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"), 911 }), 912 913 // Verify that the recurrence exception is still there 914 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 915 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", 916 "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), 917 // This time change the end time of "daily0" so that it excludes 918 // the recurrence exception. 919 new Update("daily0", new KeyValue[] { 920 new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"), 921 }), 922 // The server will cancel the out-of-range exception. 923 // It would be nice for the provider to handle this automatically, 924 // but for now simulate the server-side cancel. 925 new Update("except1", new KeyValue[] { 926 new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED), 927 }), 928 // Verify that the recurrence exception does not appear. 929 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 930 new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}), 931 }; 932 933 /** 934 * Bug 135848. Ensure that a recurrence exception is displayed even if the recurrence 935 * is not present. 936 */ 937 private Command[] mExceptionWithNoRecurrence = { 938 new Insert("except0"), 939 new QueryNumEvents(1), 940 new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", 941 new String[] {"2008-05-01T02:00:00"}), 942 }; 943 944 private EventInfo findEvent(String name) { 945 int len = mEvents.length; 946 for (int ii = 0; ii < len; ii++) { 947 EventInfo event = mEvents[ii]; 948 if (name.equals(event.mTitle)) { 949 return event; 950 } 951 } 952 return null; 953 } 954 955 public CalendarProvider2Test() { 956 super(CalendarProvider2ForTesting.class, Calendar.AUTHORITY); 957 } 958 959 @Override 960 protected void setUp() throws Exception { 961 super.setUp(); 962 963 mContext = getMockContext(); 964 mResolver = getMockContentResolver(); 965 mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds")); 966 mResolver.addProvider("sync", new MockProvider("sync")); 967 968 CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); 969 helper.wipeData(); 970 mDb = helper.getWritableDatabase(); 971 mMetaData = getProvider().mMetaData; 972 } 973 974 @Override 975 protected void tearDown() throws Exception { 976 mDb.close(); 977 mDb = null; 978 getProvider().getDatabaseHelper().close(); 979 super.tearDown(); 980 } 981 982 /** 983 * Dumps the contents of the given cursor to the log. For debugging. 984 * @param cursor the database cursor 985 */ 986 private void dumpCursor(Cursor cursor) { 987 cursor.moveToPosition(-1); 988 String[] cols = cursor.getColumnNames(); 989 990 Log.i(TAG, "dumpCursor() count: " + cursor.getCount()); 991 int index = 0; 992 while (cursor.moveToNext()) { 993 Log.i(TAG, index + " {"); 994 for (int i = 0; i < cols.length; i++) { 995 Log.i(TAG, " " + cols[i] + '=' + cursor.getString(i)); 996 } 997 Log.i(TAG, "}"); 998 index += 1; 999 } 1000 cursor.moveToPosition(-1); 1001 } 1002 1003 private int insertCal(String name, String timezone) { 1004 ContentValues m = new ContentValues(); 1005 m.put(Calendars.NAME, name); 1006 m.put(Calendars.DISPLAY_NAME, name); 1007 m.put(Calendars.COLOR, "0xff123456"); 1008 m.put(Calendars.TIMEZONE, timezone); 1009 m.put(Calendars.SELECTED, 1); 1010 m.put(Calendars.URL, CALENDAR_URL); 1011 m.put(Calendars.OWNER_ACCOUNT, "joe@joe.com"); 1012 1013 Uri url = mResolver.insert(Uri.parse("content://calendar/calendars"), m); 1014 String id = url.getLastPathSegment(); 1015 return Integer.parseInt(id); 1016 } 1017 1018 private Uri insertEvent(int calId, EventInfo event) { 1019 if (mWipe) { 1020 // Wipe instance table so it will be regenerated 1021 mMetaData.clearInstanceRange(); 1022 } 1023 ContentValues m = new ContentValues(); 1024 m.put(Events.CALENDAR_ID, calId); 1025 m.put(Events.TITLE, event.mTitle); 1026 m.put(Events.DTSTART, event.mDtstart); 1027 m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0); 1028 1029 if (event.mRrule == null) { 1030 // This is a normal event 1031 m.put(Events.DTEND, event.mDtend); 1032 } else { 1033 // This is a repeating event 1034 m.put(Events.RRULE, event.mRrule); 1035 m.put(Events.DURATION, event.mDuration); 1036 } 1037 1038 if (event.mDescription != null) { 1039 m.put(Events.DESCRIPTION, event.mDescription); 1040 } 1041 if (event.mTimezone != null) { 1042 m.put(Events.EVENT_TIMEZONE, event.mTimezone); 1043 } 1044 1045 if (event.mOriginalTitle != null) { 1046 // This is a recurrence exception. 1047 EventInfo recur = findEvent(event.mOriginalTitle); 1048 assertNotNull(recur); 1049 String syncId = String.format("%d", recur.mSyncId); 1050 m.put(Events.ORIGINAL_EVENT, syncId); 1051 m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0); 1052 m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance); 1053 } 1054 Uri url = mResolver.insert(mEventsUri, m); 1055 1056 // Create a fake _sync_id and add it to the event. Update the database 1057 // directly so that we don't trigger any validation checks in the 1058 // CalendarProvider. 1059 long id = ContentUris.parseId(url); 1060 mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id); 1061 event.mSyncId = mGlobalSyncId; 1062 mGlobalSyncId += 1; 1063 1064 return url; 1065 } 1066 1067 /** 1068 * Deletes all the events that match the given title. 1069 * @param title the given title to match events on 1070 * @return the number of rows deleted 1071 */ 1072 private int deleteMatchingEvents(String title) { 1073 Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID }, 1074 "title=?", new String[] { title }, null); 1075 int numRows = 0; 1076 while (cursor.moveToNext()) { 1077 long id = cursor.getLong(0); 1078 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 1079 numRows += mResolver.delete(uri, null, null); 1080 } 1081 cursor.close(); 1082 return numRows; 1083 } 1084 1085 /** 1086 * Updates all the events that match the given title. 1087 * @param title the given title to match events on 1088 * @return the number of rows updated 1089 */ 1090 private int updateMatchingEvents(String title, ContentValues values) { 1091 String[] projection = new String[] { 1092 Events._ID, 1093 Events.DTSTART, 1094 Events.DTEND, 1095 Events.DURATION, 1096 Events.ALL_DAY, 1097 Events.RRULE, 1098 Events.EVENT_TIMEZONE, 1099 Events.ORIGINAL_EVENT, 1100 }; 1101 Cursor cursor = mResolver.query(mEventsUri, projection, 1102 "title=?", new String[] { title }, null); 1103 int numRows = 0; 1104 while (cursor.moveToNext()) { 1105 long id = cursor.getLong(0); 1106 1107 // If any of the following fields are being changed, then we need 1108 // to include all of them. 1109 if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND) 1110 || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY) 1111 || values.containsKey(Events.RRULE) 1112 || values.containsKey(Events.EVENT_TIMEZONE) 1113 || values.containsKey(Calendar.EventsColumns.STATUS)) { 1114 long dtstart = cursor.getLong(1); 1115 long dtend = cursor.getLong(2); 1116 String duration = cursor.getString(3); 1117 boolean allDay = cursor.getInt(4) != 0; 1118 String rrule = cursor.getString(5); 1119 String timezone = cursor.getString(6); 1120 String originalEvent = cursor.getString(7); 1121 1122 if (!values.containsKey(Events.DTSTART)) { 1123 values.put(Events.DTSTART, dtstart); 1124 } 1125 // Don't add DTEND for repeating events 1126 if (!values.containsKey(Events.DTEND) && rrule == null) { 1127 values.put(Events.DTEND, dtend); 1128 } 1129 if (!values.containsKey(Events.DURATION) && duration != null) { 1130 values.put(Events.DURATION, duration); 1131 } 1132 if (!values.containsKey(Events.ALL_DAY)) { 1133 values.put(Events.ALL_DAY, allDay ? 1 : 0); 1134 } 1135 if (!values.containsKey(Events.RRULE) && rrule != null) { 1136 values.put(Events.RRULE, rrule); 1137 } 1138 if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) { 1139 values.put(Events.EVENT_TIMEZONE, timezone); 1140 } 1141 if (!values.containsKey(Events.ORIGINAL_EVENT) && originalEvent != null) { 1142 values.put(Events.ORIGINAL_EVENT, originalEvent); 1143 } 1144 } 1145 1146 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); 1147 numRows += mResolver.update(uri, values, null, null); 1148 } 1149 cursor.close(); 1150 return numRows; 1151 } 1152 1153 private void deleteAllEvents() { 1154 mDb.execSQL("DELETE FROM Events;"); 1155 mMetaData.clearInstanceRange(); 1156 } 1157 1158 public void testInsertNormalEvents() throws Exception { 1159 Cursor cursor; 1160 Uri url = null; 1161 1162 int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1163 1164 cursor = mResolver.query(mEventsUri, null, null, null, null); 1165 assertEquals(0, cursor.getCount()); 1166 cursor.close(); 1167 1168 // Keep track of the number of normal events 1169 int numEvents = 0; 1170 1171 // "begin" is the earliest start time of all the normal events, 1172 // and "end" is the latest end time of all the normal events. 1173 long begin = 0, end = 0; 1174 1175 int len = mEvents.length; 1176 for (int ii = 0; ii < len; ii++) { 1177 EventInfo event = mEvents[ii]; 1178 // Skip repeating events and recurrence exceptions 1179 if (event.mRrule != null || event.mOriginalTitle != null) { 1180 continue; 1181 } 1182 if (numEvents == 0) { 1183 begin = event.mDtstart; 1184 end = event.mDtend; 1185 } else { 1186 if (begin > event.mDtstart) { 1187 begin = event.mDtstart; 1188 } 1189 if (end < event.mDtend) { 1190 end = event.mDtend; 1191 } 1192 } 1193 url = insertEvent(calId, event); 1194 numEvents += 1; 1195 } 1196 1197 // query one 1198 cursor = mResolver.query(url, null, null, null, null); 1199 assertEquals(1, cursor.getCount()); 1200 cursor.close(); 1201 1202 // query all 1203 cursor = mResolver.query(mEventsUri, null, null, null, null); 1204 assertEquals(numEvents, cursor.getCount()); 1205 cursor.close(); 1206 1207 // Check that the Instances table has one instance of each of the 1208 // normal events. 1209 cursor = queryInstances(begin, end); 1210 assertEquals(numEvents, cursor.getCount()); 1211 cursor.close(); 1212 } 1213 1214 public void testInsertRepeatingEvents() throws Exception { 1215 Cursor cursor; 1216 Uri url = null; 1217 1218 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1219 1220 cursor = mResolver.query(mEventsUri, null, null, null, null); 1221 assertEquals(0, cursor.getCount()); 1222 cursor.close(); 1223 1224 // Keep track of the number of repeating events 1225 int numEvents = 0; 1226 1227 int len = mEvents.length; 1228 for (int ii = 0; ii < len; ii++) { 1229 EventInfo event = mEvents[ii]; 1230 // Skip normal events 1231 if (event.mRrule == null) { 1232 continue; 1233 } 1234 url = insertEvent(calId, event); 1235 numEvents += 1; 1236 } 1237 1238 // query one 1239 cursor = mResolver.query(url, null, null, null, null); 1240 assertEquals(1, cursor.getCount()); 1241 cursor.close(); 1242 1243 // query all 1244 cursor = mResolver.query(mEventsUri, null, null, null, null); 1245 assertEquals(numEvents, cursor.getCount()); 1246 cursor.close(); 1247 } 1248 1249 public void testInstanceRange() throws Exception { 1250 Cursor cursor; 1251 Uri url = null; 1252 1253 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1254 1255 cursor = mResolver.query(mEventsUri, null, null, null, null); 1256 assertEquals(0, cursor.getCount()); 1257 cursor.close(); 1258 1259 int len = mInstanceRanges.length; 1260 for (int ii = 0; ii < len; ii++) { 1261 InstanceInfo instance = mInstanceRanges[ii]; 1262 EventInfo event = instance.mEvent; 1263 url = insertEvent(calId, event); 1264 cursor = queryInstances(instance.mBegin, instance.mEnd); 1265 if (instance.mExpectedOccurrences != cursor.getCount()) { 1266 Log.e(TAG, "Test failed! Instance index: " + ii); 1267 Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription 1268 + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]" 1269 + " expected: " + instance.mExpectedOccurrences); 1270 dumpCursor(cursor); 1271 } 1272 assertEquals(instance.mExpectedOccurrences, cursor.getCount()); 1273 cursor.close(); 1274 int rows = mResolver.delete(url, null /* selection */, null /* selection args */); 1275 assertEquals(1, rows); 1276 } 1277 } 1278 1279 public void testBusyBitRange() throws Exception { 1280 Cursor cursor; 1281 Uri url = null; 1282 1283 int calId = insertCal("Calendar0", "America/Los_Angeles"); 1284 1285 cursor = mResolver.query(mEventsUri, null, null, null, null); 1286 dumpCursor(cursor); 1287 assertEquals(0, cursor.getCount()); 1288 cursor.close(); 1289 1290 int len = mBusyBitTests.length; 1291 for (int ii = 0; ii < len; ii++) { 1292 deleteAllEvents(); 1293 BusyBitInfo busyInfo = mBusyBitTests[ii]; 1294 EventInfo[] events = busyInfo.mEvents; 1295 int numEvents = events.length; 1296 for (int jj = 0; jj < numEvents; jj++) { 1297 EventInfo event = events[jj]; 1298 insertEvent(calId, event); 1299 } 1300 1301 int startDay = busyInfo.mStartDay; 1302 int numDays = busyInfo.mNumDays; 1303 int[] busybits = new int[numDays]; 1304 int[] allDayCounts = new int[numDays]; 1305 1306 if (false) { 1307 cursor = mResolver.query(mEventsUri, null, null, null, null); 1308 Log.i(TAG, "Dump of Events table, count: " + cursor.getCount()); 1309 dumpCursor(cursor); 1310 cursor.close(); 1311 1312 Time time = new Time(); 1313 time.setJulianDay(startDay); 1314 long begin = time.toMillis(true); 1315 int endDay = startDay + numDays - 1; 1316 time.setJulianDay(endDay); 1317 long end = time.toMillis(true); 1318 cursor = queryInstances(begin, end); 1319 Log.i(TAG, "Dump of Instances table, count: " + cursor.getCount() 1320 + " startDay: " + startDay + " endDay: " + endDay 1321 + " begin: " + begin + " end: " + end); 1322 dumpCursor(cursor); 1323 cursor.close(); 1324 } 1325 1326 cursor = queryBusyBits(startDay, numDays); 1327 try { 1328 int dayColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.DAY); 1329 int busybitColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.BUSYBITS); 1330 int allDayCountColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.ALL_DAY_COUNT); 1331 1332 while (cursor.moveToNext()) { 1333 int day = cursor.getInt(dayColumnIndex); 1334 int dayIndex = day - startDay; 1335 busybits[dayIndex] = cursor.getInt(busybitColumnIndex); 1336 allDayCounts[dayIndex] = cursor.getInt(allDayCountColumnIndex); 1337 } 1338 } finally { 1339 if (cursor != null) { 1340 cursor.close(); 1341 } 1342 } 1343 1344 // Compare the database busy bits with the expected busy bits 1345 for (int dayIndex = 0; dayIndex < numDays; dayIndex++) { 1346 if (busyInfo.mBusyBits[dayIndex] != busybits[dayIndex]) { 1347 String mesg = String.format("Test failed!" 1348 + " BusyBit test index: %d" 1349 + " day index: %d" 1350 + " mStartDay: %d mNumDays: %d" 1351 + " expected busybits: 0x%x was: 0x%x", 1352 ii, dayIndex, busyInfo.mStartDay, busyInfo.mNumDays, 1353 busyInfo.mBusyBits[dayIndex], busybits[dayIndex]); 1354 Log.e(TAG, mesg); 1355 1356 cursor = mResolver.query(mEventsUri, null, null, null, null); 1357 Log.i(TAG, "Dump of Events table, count: " + cursor.getCount()); 1358 dumpCursor(cursor); 1359 cursor.close(); 1360 } 1361 assertEquals(busyInfo.mBusyBits[dayIndex], busybits[dayIndex]); 1362 } 1363 1364 // Compare the database all-day counts with the expected all-day counts 1365 for (int dayIndex = 0; dayIndex < numDays; dayIndex++) { 1366 if (busyInfo.mAllDayCounts[dayIndex] != allDayCounts[dayIndex]) { 1367 String mesg = String.format("Test failed!" 1368 + " BusyBit test index: %d" 1369 + " day index: %d" 1370 + " expected all-day count: %d was: %d", 1371 ii, dayIndex, 1372 busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]); 1373 Log.e(TAG, mesg); 1374 } 1375 assertEquals(busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]); 1376 } 1377 } 1378 } 1379 1380 public void testEntityQuery() throws Exception { 1381 testInsertNormalEvents(); // To initialize 1382 1383 ContentValues reminder = new ContentValues(); 1384 reminder.put(Calendar.Reminders.EVENT_ID, 1); 1385 reminder.put(Calendar.Reminders.MINUTES, 10); 1386 reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_SMS); 1387 mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder); 1388 reminder.put(Calendar.Reminders.MINUTES, 20); 1389 mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder); 1390 1391 ContentValues extended = new ContentValues(); 1392 extended.put(Calendar.ExtendedProperties.NAME, "foo"); 1393 extended.put(Calendar.ExtendedProperties.VALUE, "bar"); 1394 extended.put(Calendar.ExtendedProperties.EVENT_ID, 2); 1395 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1396 extended.put(Calendar.ExtendedProperties.EVENT_ID, 1); 1397 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1398 extended.put(Calendar.ExtendedProperties.NAME, "foo2"); 1399 extended.put(Calendar.ExtendedProperties.VALUE, "bar2"); 1400 mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended); 1401 1402 ContentValues attendee = new ContentValues(); 1403 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1404 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1405 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1406 Calendar.Attendees.ATTENDEE_STATUS_DECLINED); 1407 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1408 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1409 Calendar.Attendees.RELATIONSHIP_PERFORMER); 1410 attendee.put(Calendar.Attendees.EVENT_ID, 3); 1411 mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1412 1413 EntityIterator ei = mResolver.queryEntities(mEventsUri, null, null, null); 1414 int count = 0; 1415 try { 1416 while (ei.hasNext()) { 1417 Entity entity = ei.next(); 1418 ContentValues values = entity.getEntityValues(); 1419 assertEquals(CALENDAR_URL, values.getAsString(Calendars.URL)); 1420 ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues(); 1421 switch (values.getAsInteger("_id")) { 1422 case 1: 1423 assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties 1424 break; 1425 case 2: 1426 assertEquals(1, subvalues.size()); // Extended properties 1427 break; 1428 case 3: 1429 assertEquals(1, subvalues.size()); // Attendees 1430 break; 1431 default: 1432 assertEquals(0, subvalues.size()); 1433 break; 1434 } 1435 count += 1; 1436 } 1437 assertEquals(5, count); 1438 } finally { 1439 ei.close(); 1440 } 1441 1442 try { 1443 ei = mResolver.queryEntities(mEventsUri, "Events._id = 3", null, null); 1444 count = 0; 1445 while (ei.hasNext()) { 1446 Entity entity = ei.next(); 1447 count += 1; 1448 } 1449 assertEquals(1, count); 1450 } finally { 1451 ei.close(); 1452 } 1453 } 1454 1455 /** 1456 * Test attendee processing 1457 * @throws Exception 1458 */ 1459 public void testAttendees() throws Exception { 1460 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1461 1462 Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); 1463 long eventId = ContentUris.parseId(eventUri); 1464 1465 ContentValues attendee = new ContentValues(); 1466 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe"); 1467 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com"); 1468 attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED); 1469 attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, 1470 Calendar.Attendees.RELATIONSHIP_ORGANIZER); 1471 attendee.put(Calendar.Attendees.EVENT_ID, eventId); 1472 Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1473 1474 Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null, 1475 "event_id=" + eventId, null, null); 1476 assertEquals(1, cursor.getCount()); 1477 cursor.close(); 1478 1479 cursor = mResolver.query(eventUri, null, null, null, null); 1480 int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS); 1481 cursor.moveToNext(); 1482 long selfAttendeeStatus = cursor.getInt(selfColumn); 1483 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus); 1484 cursor.close(); 1485 1486 // Change status to declined 1487 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1488 Calendar.Attendees.ATTENDEE_STATUS_DECLINED); 1489 mResolver.update(attendeesUri, attendee, null, null); 1490 1491 cursor = mResolver.query(eventUri, null, null, null, null); 1492 cursor.moveToNext(); 1493 selfAttendeeStatus = cursor.getInt(selfColumn); 1494 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 1495 cursor.close(); 1496 1497 // Add another attendee 1498 attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude"); 1499 attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com"); 1500 attendee.put(Calendar.Attendees.ATTENDEE_STATUS, 1501 Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED); 1502 mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee); 1503 1504 cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null, 1505 "event_id=" + mCalendarId, null, null); 1506 assertEquals(2, cursor.getCount()); 1507 cursor.close(); 1508 1509 cursor = mResolver.query(eventUri, null, null, null, null); 1510 cursor.moveToNext(); 1511 selfAttendeeStatus = cursor.getInt(selfColumn); 1512 assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); 1513 cursor.close(); 1514 } 1515 1516 /** 1517 * Run commands, wiping instance table at each step. 1518 * This tests full instance expansion. 1519 * @throws Exception 1520 */ 1521 public void testCommandSequences1() throws Exception { 1522 commandSequences(true); 1523 } 1524 1525 /** 1526 * Run commands normally. 1527 * This tests incremental instance expansion. 1528 * @throws Exception 1529 */ 1530 public void testCommandSequences2() throws Exception { 1531 commandSequences(false); 1532 } 1533 1534 /** 1535 * Run thorough set of command sequences 1536 * @param wipe true if instances should be wiped and regenerated 1537 * @throws Exception 1538 */ 1539 private void commandSequences(boolean wipe) throws Exception { 1540 Cursor cursor; 1541 Uri url = null; 1542 mWipe = wipe; // Set global flag 1543 1544 mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); 1545 1546 cursor = mResolver.query(mEventsUri, null, null, null, null); 1547 dumpCursor(cursor); 1548 assertEquals(0, cursor.getCount()); 1549 cursor.close(); 1550 Command[] commands; 1551 1552 Log.i(TAG, "Normal insert/delete"); 1553 commands = mNormalInsertDelete; 1554 for (Command command : commands) { 1555 command.execute(); 1556 } 1557 1558 deleteAllEvents(); 1559 1560 Log.i(TAG, "All-day insert/delete"); 1561 commands = mAlldayInsertDelete; 1562 for (Command command : commands) { 1563 command.execute(); 1564 } 1565 1566 deleteAllEvents(); 1567 1568 Log.i(TAG, "Recurring insert/delete"); 1569 commands = mRecurringInsertDelete; 1570 for (Command command : commands) { 1571 command.execute(); 1572 } 1573 1574 deleteAllEvents(); 1575 1576 Log.i(TAG, "Exception with truncated recurrence"); 1577 commands = mExceptionWithTruncatedRecurrence; 1578 for (Command command : commands) { 1579 command.execute(); 1580 } 1581 1582 deleteAllEvents(); 1583 1584 Log.i(TAG, "Exception with moved recurrence"); 1585 commands = mExceptionWithMovedRecurrence; 1586 for (Command command : commands) { 1587 command.execute(); 1588 } 1589 1590 deleteAllEvents(); 1591 1592 Log.i(TAG, "Exception with cancel"); 1593 commands = mCancelInstance; 1594 for (Command command : commands) { 1595 command.execute(); 1596 } 1597 1598 deleteAllEvents(); 1599 1600 Log.i(TAG, "Exception with moved recurrence2"); 1601 commands = mExceptionWithMovedRecurrence2; 1602 for (Command command : commands) { 1603 command.execute(); 1604 } 1605 1606 deleteAllEvents(); 1607 1608 Log.i(TAG, "Exception with no recurrence"); 1609 commands = mExceptionWithNoRecurrence; 1610 for (Command command : commands) { 1611 command.execute(); 1612 } 1613 } 1614 1615 /** 1616 * Test Time toString. 1617 * @throws Exception 1618 */ 1619 // Suppressed because toString currently hangs. 1620 @Suppress 1621 public void testTimeToString() throws Exception { 1622 Time time = new Time(Time.TIMEZONE_UTC); 1623 String str = "2039-01-01T23:00:00.000Z"; 1624 String result = "20390101T230000UTC(0,0,0,-1,0)"; 1625 time.parse3339(str); 1626 assertEquals(result, time.toString()); 1627 } 1628 1629 private Cursor queryInstances(long begin, long end) { 1630 Uri url = Uri.parse("content://calendar/instances/when/" + begin + "/" + end); 1631 return mResolver.query(url, null, null, null, null); 1632 } 1633 1634 private Cursor queryBusyBits(int startDay, int numDays) { 1635 int endDay = startDay + numDays - 1; 1636 Uri url = Uri.parse("content://calendar/busybits/when/" + startDay + "/" + endDay); 1637 return mResolver.query(url, null, null, null, null); 1638 } 1639 1640 protected static class MockProvider extends ContentProvider { 1641 1642 private String mAuthority; 1643 1644 private int mNumItems = 0; 1645 1646 public MockProvider(String authority) { 1647 mAuthority = authority; 1648 } 1649 1650 @Override 1651 public boolean onCreate() { 1652 return true; 1653 } 1654 1655 @Override 1656 public Cursor query(Uri uri, String[] projection, String selection, 1657 String[] selectionArgs, String sortOrder) { 1658 return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>()); 1659 } 1660 1661 @Override 1662 public String getType(Uri uri) { 1663 throw new UnsupportedOperationException(); 1664 } 1665 1666 @Override 1667 public Uri insert(Uri uri, ContentValues values) { 1668 mNumItems++; 1669 return Uri.parse("content://" + mAuthority + "/" + mNumItems); 1670 } 1671 1672 @Override 1673 public int delete(Uri uri, String selection, String[] selectionArgs) { 1674 return 0; 1675 } 1676 1677 @Override 1678 public int update(Uri uri, ContentValues values, String selection, 1679 String[] selectionArgs) { 1680 return 0; 1681 } 1682 } 1683} 1684