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