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