CalendarProvider2Test.java revision 30c0d7ae9d4c410254dc4106fa4ffa692a7e6706
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.common.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.Calendars;
29import android.provider.Calendar.Events;
30import android.provider.Calendar.EventsEntity;
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 = Events.CONTENT_URI;
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    /**
519     * This is the main table of events.  The events in this table are
520     * referred to by name in other places.
521     */
522    private EventInfo[] mEvents = {
523            new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false),
524            new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false),
525            new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false),
526            new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true),
527            new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true),
528            new EventInfo("daily0", "daily from 5/1/2008 12am to 1am",
529                    "2008-05-01T00:00:00", "2008-05-01T01:00:00",
530                    "FREQ=DAILY;WKST=SU", false),
531            new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am",
532                    "2008-05-01T08:30:00", "2008-05-01T09:30:00",
533                    "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false),
534            new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am",
535                    "2008-05-01T08:45:00", "2008-05-01T09:15:00",
536                    "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false),
537            new EventInfo("allday daily0", "all-day daily from 5/1/2008",
538                    "2008-05-01", null,
539                    "FREQ=DAILY;WKST=SU", true),
540            new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008",
541                    "2008-05-01", null,
542                    "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true),
543            new EventInfo("allday weekly0", "all-day weekly from 5/1/2008",
544                    "2008-05-01", null,
545                    "FREQ=WEEKLY;WKST=SU", true),
546            new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008",
547                    "2008-05-01", "2008-05-03",
548                    "FREQ=WEEKLY;WKST=SU", true),
549            new EventInfo("allday yearly0", "all-day yearly on 5/1/2008",
550                    "2008-05-01T", null,
551                    "FREQ=YEARLY;WKST=SU", true),
552            new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm",
553                    "2008-05-06T13:00:00", "2008-05-06T14:00:00",
554                    "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false),
555            new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm",
556                    "2008-05-06T14:30:00", "2008-05-06T15:30:00",
557                    "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false),
558            new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm",
559                    "2008-05-20T15:00:00", "2008-05-20T16:00:00",
560                    "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false),
561            new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am",
562                    "2008-05-01T00:00:00", "2008-05-01T00:10:00",
563                    "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false),
564            new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight",
565                    "2008-05-31T23:00:00", "2008-06-01T00:00:00",
566                    "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false),
567            new EventInfo("daily0", "2008-05-01T00:00:00",
568                    "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am",
569                    "2008-05-01T02:00:00", "2008-05-01T01:03:00", false),
570            new EventInfo("daily0", "2008-05-03T00:00:00",
571                    "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am",
572                    "2008-05-03T02:00:00", "2008-05-03T01:03:00", false),
573            new EventInfo("daily0", "2008-05-02T00:00:00",
574                    "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008",
575                    "2008-01-02T00:00:00", "2008-01-02T01:00:00", false),
576            new EventInfo("weekly0", "2008-05-13T13:00:00",
577                    "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm",
578                    "2008-12-11T13:00:00", "2008-12-11T14:00:00", false),
579            new EventInfo("weekly0", "2008-05-13T13:00:00",
580                    "cancel0", "weekly0 exception for 5/13/2008 1pm",
581                    "2008-05-13T13:00:00", "2008-05-13T14:00:00", false),
582            new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm",
583                    "2008-05-01T13:00:00", "2008-05-01T14:00:00",
584                    "FREQ=YEARLY;WKST=SU", false),
585    };
586
587    /**
588     * This table is used to create repeating events and then check that the
589     * number of instances within a given range matches the expected number
590     * of instances.
591     */
592    private InstanceInfo[] mInstanceRanges = {
593            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1),
594            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
595            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2),
596            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2),
597            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
598            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1),
599            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2),
600            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31),
601            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32),
602
603            new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
604            new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2),
605
606            new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
607            new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3),
608
609            new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7),
610            new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3),
611            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1),
612            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2),
613            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5),
614            new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5),
615            new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1),
616            new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2),
617
618            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
619            new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
620            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4),
621            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8),
622
623            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
624            new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
625            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2),
626            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4),
627
628            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0),
629            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1),
630            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0),
631            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0),
632            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1),
633            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
634            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2),
635
636            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
637            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
638            new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1),
639            new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0),
640            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2),
641
642            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0),
643            new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1),
644            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1),
645            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2),
646
647            new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1),
648            new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2),
649    };
650
651    /**
652     * This sequence of commands inserts and deletes some events.
653     */
654    private Command[] mNormalInsertDelete = {
655            new Insert("normal0"),
656            new Insert("normal1"),
657            new Insert("normal2"),
658            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3),
659            new Delete("normal1", 1),
660            new QueryNumEvents(2),
661            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2),
662            new Delete("normal1", 0),
663            new Delete("normal2", 1),
664            new QueryNumEvents(1),
665            new Delete("normal0", 1),
666            new QueryNumEvents(0),
667    };
668
669    /**
670     * This sequence of commands inserts and deletes some all-day events.
671     */
672    private Command[] mAlldayInsertDelete = {
673            new Insert("allday0"),
674            new Insert("allday1"),
675            new QueryNumEvents(2),
676            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0),
677            new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2),
678            new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
679            new Delete("allday0", 1),
680            new QueryNumEvents(1),
681            new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
682            new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
683            new Delete("allday1", 1),
684            new QueryNumEvents(0),
685    };
686
687    /**
688     * This sequence of commands inserts and deletes some repeating events.
689     */
690    private Command[] mRecurringInsertDelete = {
691            new Insert("daily0"),
692            new Insert("daily1"),
693            new QueryNumEvents(2),
694            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3),
695            new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2),
696            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6),
697            new Delete("daily1", 1),
698            new QueryNumEvents(1),
699            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2),
700            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4),
701            new Delete("daily0", 1),
702            new QueryNumEvents(0),
703    };
704
705    /**
706     * This sequence of commands creates a recurring event with a recurrence
707     * exception that moves an event outside the expansion window.  It checks that the
708     * recurrence exception does not occur in the Instances database table.
709     * Bug 1642665
710     */
711    private Command[] mExceptionWithMovedRecurrence = {
712            new Insert("daily0"),
713            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
714                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
715                            "2008-05-03T00:00:00", }),
716            new Insert("except2"),
717            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
718                    new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}),
719    };
720
721    /**
722     * This sequence of commands deletes (cancels) one instance of a recurrence.
723     */
724    private Command[] mCancelInstance = {
725            new Insert("weekly0"),
726            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
727                    new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00",
728                            "2008-05-20T13:00:00", }),
729            new Insert("cancel0"),
730            new Update("cancel0", new KeyValue[] {
731                    new KeyValue(Calendar.EventsColumns.STATUS,
732                            "" + Calendar.EventsColumns.STATUS_CANCELED),
733            }),
734            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
735                    new String[] {"2008-05-06T13:00:00",
736                            "2008-05-20T13:00:00", }),
737    };
738    /**
739     * This sequence of commands creates a recurring event with a recurrence
740     * exception that moves an event from outside the expansion window into the
741     * expansion window.
742     */
743    private Command[] mExceptionWithMovedRecurrence2 = {
744            new Insert("weekly0"),
745            new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
746                    new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
747                            "2008-12-16T13:00:00", }),
748            new Insert("except3"),
749            new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
750                    new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
751                            "2008-12-11T13:00:00", "2008-12-16T13:00:00", }),
752    };
753    /**
754     * This sequence of commands creates a recurring event with a recurrence
755     * exception and then changes the end time of the recurring event.  It then
756     * checks that the recurrence exception does not occur in the Instances
757     * database table.
758     */
759    private Command[]
760            mExceptionWithTruncatedRecurrence = {
761            new Insert("daily0"),
762            // Verify 4 occurrences of the "daily0" repeating event
763            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
764                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
765                            "2008-05-03T00:00:00", "2008-05-04T00:00:00"}),
766            new Insert("except1"),
767            new QueryNumEvents(2),
768
769            // Verify that one of the 4 occurrences has its start time changed
770            // so that it now matches the recurrence exception.
771            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
772                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
773                            "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
774
775            // Change the end time of "daily0" but it still includes the
776            // recurrence exception.
777            new Update("daily0", new KeyValue[] {
778                    new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"),
779            }),
780
781            // Verify that the recurrence exception is still there
782            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
783                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
784                            "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
785            // This time change the end time of "daily0" so that it excludes
786            // the recurrence exception.
787            new Update("daily0", new KeyValue[] {
788                    new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"),
789            }),
790            // The server will cancel the out-of-range exception.
791            // It would be nice for the provider to handle this automatically,
792            // but for now simulate the server-side cancel.
793            new Update("except1", new KeyValue[] {
794                    new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED),
795            }),
796            // Verify that the recurrence exception does not appear.
797            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
798                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}),
799    };
800
801    /**
802     * Bug 135848.  Ensure that a recurrence exception is displayed even if the recurrence
803     * is not present.
804     */
805    private Command[] mExceptionWithNoRecurrence = {
806            new Insert("except0"),
807            new QueryNumEvents(1),
808            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
809                    new String[] {"2008-05-01T02:00:00"}),
810    };
811
812    private EventInfo findEvent(String name) {
813        int len = mEvents.length;
814        for (int ii = 0; ii < len; ii++) {
815            EventInfo event = mEvents[ii];
816            if (name.equals(event.mTitle)) {
817                return event;
818            }
819        }
820        return null;
821    }
822
823    public CalendarProvider2Test() {
824        super(CalendarProvider2ForTesting.class, Calendar.AUTHORITY);
825    }
826
827    @Override
828    protected void setUp() throws Exception {
829        super.setUp();
830
831        mContext = getMockContext();
832        mResolver = getMockContentResolver();
833        mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
834        mResolver.addProvider("sync", new MockProvider("sync"));
835
836        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
837        helper.wipeData();
838        mDb = helper.getWritableDatabase();
839        mMetaData = getProvider().mMetaData;
840    }
841
842    @Override
843    protected void tearDown() throws Exception {
844        mDb.close();
845        mDb = null;
846        getProvider().getDatabaseHelper().close();
847        super.tearDown();
848    }
849
850    /**
851     * Dumps the contents of the given cursor to the log.  For debugging.
852     * @param cursor the database cursor
853     */
854    private void dumpCursor(Cursor cursor) {
855        cursor.moveToPosition(-1);
856        String[] cols = cursor.getColumnNames();
857
858        Log.i(TAG, "dumpCursor() count: " + cursor.getCount());
859        int index = 0;
860        while (cursor.moveToNext()) {
861            Log.i(TAG, index + " {");
862            for (int i = 0; i < cols.length; i++) {
863                Log.i(TAG, "    " + cols[i] + '=' + cursor.getString(i));
864            }
865            Log.i(TAG, "}");
866            index += 1;
867        }
868        cursor.moveToPosition(-1);
869    }
870
871    private int insertCal(String name, String timezone) {
872        return insertCal(name, timezone, "joe@joe.com");
873    }
874
875    private int insertCal(String name, String timezone, String account) {
876        ContentValues m = new ContentValues();
877        m.put(Calendars.NAME, name);
878        m.put(Calendars.DISPLAY_NAME, name);
879        m.put(Calendars.COLOR, "0xff123456");
880        m.put(Calendars.TIMEZONE, timezone);
881        m.put(Calendars.SELECTED, 1);
882        m.put(Calendars.URL, CALENDAR_URL);
883        m.put(Calendars.OWNER_ACCOUNT, account);
884        m.put(Calendars._SYNC_ACCOUNT,  account);
885        m.put(Calendars._SYNC_ACCOUNT_TYPE,  "com.google");
886
887        Uri url = mResolver.insert(Calendar.Calendars.CONTENT_URI, m);
888        String id = url.getLastPathSegment();
889        return Integer.parseInt(id);
890    }
891
892    private Uri insertEvent(int calId, EventInfo event) {
893        if (mWipe) {
894            // Wipe instance table so it will be regenerated
895            mMetaData.clearInstanceRange();
896        }
897        ContentValues m = new ContentValues();
898        m.put(Events.CALENDAR_ID, calId);
899        m.put(Events.TITLE, event.mTitle);
900        m.put(Events.DTSTART, event.mDtstart);
901        m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
902
903        if (event.mRrule == null) {
904            // This is a normal event
905            m.put(Events.DTEND, event.mDtend);
906        } else {
907            // This is a repeating event
908            m.put(Events.RRULE, event.mRrule);
909            m.put(Events.DURATION, event.mDuration);
910        }
911
912        if (event.mDescription != null) {
913            m.put(Events.DESCRIPTION, event.mDescription);
914        }
915        if (event.mTimezone != null) {
916            m.put(Events.EVENT_TIMEZONE, event.mTimezone);
917        }
918
919        if (event.mOriginalTitle != null) {
920            // This is a recurrence exception.
921            EventInfo recur = findEvent(event.mOriginalTitle);
922            assertNotNull(recur);
923            String syncId = String.format("%d", recur.mSyncId);
924            m.put(Events.ORIGINAL_EVENT, syncId);
925            m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0);
926            m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
927        }
928        Uri url = mResolver.insert(mEventsUri, m);
929
930        // Create a fake _sync_id and add it to the event.  Update the database
931        // directly so that we don't trigger any validation checks in the
932        // CalendarProvider.
933        long id = ContentUris.parseId(url);
934        mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id);
935        event.mSyncId = mGlobalSyncId;
936        mGlobalSyncId += 1;
937
938        return url;
939    }
940
941    /**
942     * Deletes all the events that match the given title.
943     * @param title the given title to match events on
944     * @return the number of rows deleted
945     */
946    private int deleteMatchingEvents(String title) {
947        Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID },
948                "title=?", new String[] { title }, null);
949        int numRows = 0;
950        while (cursor.moveToNext()) {
951            long id = cursor.getLong(0);
952            // Do delete as a sync adapter so event is really deleted, not just marked
953            // as deleted.
954            Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true);
955            numRows += mResolver.delete(uri, null, null);
956        }
957        cursor.close();
958        return numRows;
959    }
960
961    /**
962     * Updates all the events that match the given title.
963     * @param title the given title to match events on
964     * @return the number of rows updated
965     */
966    private int updateMatchingEvents(String title, ContentValues values) {
967        String[] projection = new String[] {
968                Events._ID,
969                Events.DTSTART,
970                Events.DTEND,
971                Events.DURATION,
972                Events.ALL_DAY,
973                Events.RRULE,
974                Events.EVENT_TIMEZONE,
975                Events.ORIGINAL_EVENT,
976        };
977        Cursor cursor = mResolver.query(mEventsUri, projection,
978                "title=?", new String[] { title }, null);
979        int numRows = 0;
980        while (cursor.moveToNext()) {
981            long id = cursor.getLong(0);
982
983            // If any of the following fields are being changed, then we need
984            // to include all of them.
985            if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
986                    || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
987                    || values.containsKey(Events.RRULE)
988                    || values.containsKey(Events.EVENT_TIMEZONE)
989                    || values.containsKey(Calendar.EventsColumns.STATUS)) {
990                long dtstart = cursor.getLong(1);
991                long dtend = cursor.getLong(2);
992                String duration = cursor.getString(3);
993                boolean allDay = cursor.getInt(4) != 0;
994                String rrule = cursor.getString(5);
995                String timezone = cursor.getString(6);
996                String originalEvent = cursor.getString(7);
997
998                if (!values.containsKey(Events.DTSTART)) {
999                    values.put(Events.DTSTART, dtstart);
1000                }
1001                // Don't add DTEND for repeating events
1002                if (!values.containsKey(Events.DTEND) && rrule == null) {
1003                    values.put(Events.DTEND, dtend);
1004                }
1005                if (!values.containsKey(Events.DURATION) && duration != null) {
1006                    values.put(Events.DURATION, duration);
1007                }
1008                if (!values.containsKey(Events.ALL_DAY)) {
1009                    values.put(Events.ALL_DAY, allDay ? 1 : 0);
1010                }
1011                if (!values.containsKey(Events.RRULE) && rrule != null) {
1012                    values.put(Events.RRULE, rrule);
1013                }
1014                if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
1015                    values.put(Events.EVENT_TIMEZONE, timezone);
1016                }
1017                if (!values.containsKey(Events.ORIGINAL_EVENT) && originalEvent != null) {
1018                    values.put(Events.ORIGINAL_EVENT, originalEvent);
1019                }
1020            }
1021
1022            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1023            numRows += mResolver.update(uri, values, null, null);
1024        }
1025        cursor.close();
1026        return numRows;
1027    }
1028
1029    private void deleteAllEvents() {
1030        mDb.execSQL("DELETE FROM Events;");
1031        mMetaData.clearInstanceRange();
1032    }
1033
1034    public void testInsertNormalEvents() throws Exception {
1035        Cursor cursor;
1036        Uri url = null;
1037
1038        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1039
1040        cursor = mResolver.query(mEventsUri, null, null, null, null);
1041        assertEquals(0, cursor.getCount());
1042        cursor.close();
1043
1044        // Keep track of the number of normal events
1045        int numEvents = 0;
1046
1047        // "begin" is the earliest start time of all the normal events,
1048        // and "end" is the latest end time of all the normal events.
1049        long begin = 0, end = 0;
1050
1051        int len = mEvents.length;
1052        for (int ii = 0; ii < len; ii++) {
1053            EventInfo event = mEvents[ii];
1054            // Skip repeating events and recurrence exceptions
1055            if (event.mRrule != null || event.mOriginalTitle != null) {
1056                continue;
1057            }
1058            if (numEvents == 0) {
1059                begin = event.mDtstart;
1060                end = event.mDtend;
1061            } else {
1062                if (begin > event.mDtstart) {
1063                    begin = event.mDtstart;
1064                }
1065                if (end < event.mDtend) {
1066                    end = event.mDtend;
1067                }
1068            }
1069            url = insertEvent(calId, event);
1070            numEvents += 1;
1071        }
1072
1073        // query one
1074        cursor = mResolver.query(url, null, null, null, null);
1075        assertEquals(1, cursor.getCount());
1076        cursor.close();
1077
1078        // query all
1079        cursor = mResolver.query(mEventsUri, null, null, null, null);
1080        assertEquals(numEvents, cursor.getCount());
1081        cursor.close();
1082
1083        // Check that the Instances table has one instance of each of the
1084        // normal events.
1085        cursor = queryInstances(begin, end);
1086        assertEquals(numEvents, cursor.getCount());
1087        cursor.close();
1088    }
1089
1090    public void testInsertRepeatingEvents() throws Exception {
1091        Cursor cursor;
1092        Uri url = null;
1093
1094        int calId = insertCal("Calendar0", "America/Los_Angeles");
1095
1096        cursor = mResolver.query(mEventsUri, null, null, null, null);
1097        assertEquals(0, cursor.getCount());
1098        cursor.close();
1099
1100        // Keep track of the number of repeating events
1101        int numEvents = 0;
1102
1103        int len = mEvents.length;
1104        for (int ii = 0; ii < len; ii++) {
1105            EventInfo event = mEvents[ii];
1106            // Skip normal events
1107            if (event.mRrule == null) {
1108                continue;
1109            }
1110            url = insertEvent(calId, event);
1111            numEvents += 1;
1112        }
1113
1114        // query one
1115        cursor = mResolver.query(url, null, null, null, null);
1116        assertEquals(1, cursor.getCount());
1117        cursor.close();
1118
1119        // query all
1120        cursor = mResolver.query(mEventsUri, null, null, null, null);
1121        assertEquals(numEvents, cursor.getCount());
1122        cursor.close();
1123    }
1124
1125    public void testInstanceRange() throws Exception {
1126        Cursor cursor;
1127        Uri url = null;
1128
1129        int calId = insertCal("Calendar0", "America/Los_Angeles");
1130
1131        cursor = mResolver.query(mEventsUri, null, null, null, null);
1132        assertEquals(0, cursor.getCount());
1133        cursor.close();
1134
1135        int len = mInstanceRanges.length;
1136        for (int ii = 0; ii < len; ii++) {
1137            InstanceInfo instance = mInstanceRanges[ii];
1138            EventInfo event = instance.mEvent;
1139            url = insertEvent(calId, event);
1140            cursor = queryInstances(instance.mBegin, instance.mEnd);
1141            if (instance.mExpectedOccurrences != cursor.getCount()) {
1142                Log.e(TAG, "Test failed! Instance index: " + ii);
1143                Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription
1144                        + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]"
1145                        + " expected: " + instance.mExpectedOccurrences);
1146                dumpCursor(cursor);
1147            }
1148            assertEquals(instance.mExpectedOccurrences, cursor.getCount());
1149            cursor.close();
1150            // Delete as sync_adapter so event is really deleted.
1151            int rows = mResolver.delete(updatedUri(url, true),
1152                    null /* selection */, null /* selection args */);
1153            assertEquals(1, rows);
1154        }
1155    }
1156
1157    public void testEntityQuery() throws Exception {
1158        testInsertNormalEvents(); // To initialize
1159
1160        ContentValues reminder = new ContentValues();
1161        reminder.put(Calendar.Reminders.EVENT_ID, 1);
1162        reminder.put(Calendar.Reminders.MINUTES, 10);
1163        reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_SMS);
1164        mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
1165        reminder.put(Calendar.Reminders.MINUTES, 20);
1166        mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
1167
1168        ContentValues extended = new ContentValues();
1169        extended.put(Calendar.ExtendedProperties.NAME, "foo");
1170        extended.put(Calendar.ExtendedProperties.VALUE, "bar");
1171        extended.put(Calendar.ExtendedProperties.EVENT_ID, 2);
1172        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1173        extended.put(Calendar.ExtendedProperties.EVENT_ID, 1);
1174        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1175        extended.put(Calendar.ExtendedProperties.NAME, "foo2");
1176        extended.put(Calendar.ExtendedProperties.VALUE, "bar2");
1177        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1178
1179        ContentValues attendee = new ContentValues();
1180        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1181        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1182        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1183                Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
1184        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1185        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1186                Calendar.Attendees.RELATIONSHIP_PERFORMER);
1187        attendee.put(Calendar.Attendees.EVENT_ID, 3);
1188        mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1189
1190        EntityIterator ei = EventsEntity.newEntityIterator(
1191                mResolver.query(EventsEntity.CONTENT_URI, null, null, null, null), mResolver);
1192        int count = 0;
1193        try {
1194            while (ei.hasNext()) {
1195                Entity entity = ei.next();
1196                ContentValues values = entity.getEntityValues();
1197                assertEquals(CALENDAR_URL, values.getAsString(Calendars.URL));
1198                ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
1199                switch (values.getAsInteger("_id")) {
1200                    case 1:
1201                        assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties
1202                        break;
1203                    case 2:
1204                        assertEquals(1, subvalues.size()); // Extended properties
1205                        ContentValues subContentValues = subvalues.get(0).values;
1206                        String name = subContentValues.getAsString(
1207                                Calendar.ExtendedProperties.NAME);
1208                        String value = subContentValues.getAsString(
1209                                Calendar.ExtendedProperties.VALUE);
1210                        assertEquals("foo", name);
1211                        assertEquals("bar", value);
1212                        break;
1213                    case 3:
1214                        assertEquals(1, subvalues.size()); // Attendees
1215                        break;
1216                    default:
1217                        assertEquals(0, subvalues.size());
1218                        break;
1219                }
1220                count += 1;
1221            }
1222            assertEquals(5, count);
1223        } finally {
1224            ei.close();
1225        }
1226
1227        ei = EventsEntity.newEntityIterator(
1228                    mResolver.query(EventsEntity.CONTENT_URI, null, "_id = 3", null, null),
1229                mResolver);
1230        try {
1231            count = 0;
1232            while (ei.hasNext()) {
1233                Entity entity = ei.next();
1234                count += 1;
1235            }
1236            assertEquals(1, count);
1237        } finally {
1238            ei.close();
1239        }
1240    }
1241
1242    public void testDeleteCalendar() throws Exception {
1243        int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
1244        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1245        insertEvent(calendarId0, mEvents[0]);
1246        insertEvent(calendarId1, mEvents[1]);
1247        // Should have 2 calendars and 2 events
1248        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 2);
1249        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 2);
1250
1251        int deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI,
1252                "ownerAccount='user2@google.com'", null /* selectionArgs */);
1253
1254        assertEquals(1, deletes);
1255        // Should have 1 calendar and 1 event
1256        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 1);
1257        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 1);
1258
1259        deletes = mResolver.delete(Uri.withAppendedPath(Calendar.Calendars.CONTENT_URI,
1260                String.valueOf(calendarId0)),
1261                null /* selection*/ , null /* selectionArgs */);
1262
1263        assertEquals(1, deletes);
1264        // Should have 0 calendars and 0 events
1265        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 0);
1266        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 0);
1267
1268        deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI,
1269                "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */);
1270
1271        assertEquals(0, deletes);
1272    }
1273
1274    public void testCalendarAlerts() throws Exception {
1275        // This projection is from AlertActivity; want to make sure it works.
1276        String[] projection = new String[] {
1277                Calendar.CalendarAlerts._ID,              // 0
1278                Calendar.CalendarAlerts.TITLE,            // 1
1279                Calendar.CalendarAlerts.EVENT_LOCATION,   // 2
1280                Calendar.CalendarAlerts.ALL_DAY,          // 3
1281                Calendar.CalendarAlerts.BEGIN,            // 4
1282                Calendar.CalendarAlerts.END,              // 5
1283                Calendar.CalendarAlerts.EVENT_ID,         // 6
1284                Calendar.CalendarAlerts.COLOR,            // 7
1285                Calendar.CalendarAlerts.RRULE,            // 8
1286                Calendar.CalendarAlerts.HAS_ALARM,        // 9
1287                Calendar.CalendarAlerts.STATE,            // 10
1288                Calendar.CalendarAlerts.ALARM_TIME,       // 11
1289        };
1290        testInsertNormalEvents(); // To initialize
1291
1292        Uri alertUri = Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1293                2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
1294        Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1295                2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */);
1296
1297        // Regular query
1298        Cursor cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI, projection,
1299                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1300
1301        assertEquals(2, cursor.getCount());
1302        cursor.close();
1303
1304        // Instance query
1305        cursor = mResolver.query(alertUri, projection,
1306                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1307
1308        assertEquals(1, cursor.getCount());
1309        cursor.close();
1310
1311        // Grouped by event query
1312        cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI_BY_INSTANCE, projection,
1313                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1314
1315        assertEquals(1, cursor.getCount());
1316        cursor.close();
1317    }
1318
1319    /**
1320     * Test attendee processing
1321     * @throws Exception
1322     */
1323    public void testAttendees() throws Exception {
1324        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1325
1326        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1327        long eventId = ContentUris.parseId(eventUri);
1328
1329        ContentValues attendee = new ContentValues();
1330        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1331        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1332        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1333        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1334                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
1335        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1336        Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1337
1338        Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1339                "event_id=" + eventId, null, null);
1340        assertEquals(1, cursor.getCount());
1341        cursor.close();
1342
1343        cursor = mResolver.query(eventUri, null, null, null, null);
1344        int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS);
1345        cursor.moveToNext();
1346        long selfAttendeeStatus = cursor.getInt(selfColumn);
1347        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
1348        cursor.close();
1349
1350        // Change status to declined
1351        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1352                Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
1353        mResolver.update(attendeesUri, attendee, null, null);
1354
1355        cursor = mResolver.query(eventUri, null, null, null, null);
1356        cursor.moveToNext();
1357        selfAttendeeStatus = cursor.getInt(selfColumn);
1358        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1359        cursor.close();
1360
1361        // Add another attendee
1362        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude");
1363        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
1364        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1365                Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED);
1366        mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1367
1368        cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1369                "event_id=" + mCalendarId, null, null);
1370        assertEquals(2, cursor.getCount());
1371        cursor.close();
1372
1373        cursor = mResolver.query(eventUri, null, null, null, null);
1374        cursor.moveToNext();
1375        selfAttendeeStatus = cursor.getInt(selfColumn);
1376        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1377        cursor.close();
1378    }
1379
1380
1381    /**
1382     * Test the event's _sync_dirty status and clear it.
1383     * @param eventId event to fetch.
1384     * @param wanted the wanted _sync_dirty status
1385     */
1386    private void testAndClearDirty(long eventId, int wanted) {
1387        Cursor cursor = mResolver.query(
1388                ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId),
1389                null, null, null, null);
1390        try {
1391            assertEquals("Event count", 1, cursor.getCount());
1392            cursor.moveToNext();
1393            int dirty = cursor.getInt(cursor.getColumnIndex(Calendar.Events._SYNC_DIRTY));
1394            assertEquals("dirty flag", wanted, dirty);
1395            if (dirty == 1) {
1396                // Have to access database directly since provider will set dirty again.
1397                mDb.execSQL("UPDATE Events SET _sync_dirty=0 WHERE _id=" + eventId);
1398            }
1399        } finally {
1400            cursor.close();
1401        }
1402    }
1403
1404    /**
1405     * Test the count of results from a query.
1406     * @param uri The URI to query
1407     * @param where The where string or null.
1408     * @param wanted The number of results wanted.  An assertion is thrown if it doesn't match.
1409     */
1410    private void testQueryCount(Uri uri, String where, int wanted) {
1411        Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */,
1412                null /* sortOrder */);
1413        try {
1414            assertEquals("query results", wanted, cursor.getCount());
1415        } finally {
1416            cursor.close();
1417        }
1418    }
1419
1420    /**
1421     * Test dirty flag processing.
1422     * @throws Exception
1423     */
1424    public void testDirty() throws Exception {
1425        internalTestDirty(false);
1426    }
1427
1428    /**
1429     * Test dirty flag processing for updates from a sync adapter.
1430     * @throws Exception
1431     */
1432    public void testDirtyWithSyncAdapter() throws Exception {
1433        internalTestDirty(true);
1434    }
1435
1436    /**
1437     * Add CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.
1438     */
1439    private Uri updatedUri(Uri uri, boolean syncAdapter) {
1440        if (syncAdapter) {
1441            return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
1442                    .build();
1443        } else {
1444            return uri;
1445        }
1446    }
1447
1448    /**
1449     * Test dirty flag processing either for syncAdapter operations or client operations.
1450     * The main difference is syncAdapter operations don't set the dirty bit.
1451     */
1452    private void internalTestDirty(boolean syncAdapter) throws Exception {
1453        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1454
1455        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1456
1457        long eventId = ContentUris.parseId(eventUri);
1458        testAndClearDirty(eventId, 1);
1459
1460        ContentValues attendee = new ContentValues();
1461        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1462        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1463        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1464        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1465                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
1466        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1467
1468        Uri attendeeUri = mResolver.insert(
1469                updatedUri(Calendar.Attendees.CONTENT_URI, syncAdapter),
1470                attendee);
1471        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1472        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
1473
1474        ContentValues reminder = new ContentValues();
1475        reminder.put(Calendar.Reminders.MINUTES, 10);
1476        reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_EMAIL);
1477        reminder.put(Calendar.Attendees.EVENT_ID, eventId);
1478
1479        Uri reminderUri = mResolver.insert(
1480                updatedUri(Calendar.Reminders.CONTENT_URI, syncAdapter), reminder);
1481        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1482        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
1483
1484        ContentValues alert = new ContentValues();
1485        alert.put(Calendar.CalendarAlerts.BEGIN, 10);
1486        alert.put(Calendar.CalendarAlerts.END, 20);
1487        alert.put(Calendar.CalendarAlerts.ALARM_TIME, 30);
1488        alert.put(Calendar.CalendarAlerts.CREATION_TIME, 40);
1489        alert.put(Calendar.CalendarAlerts.RECEIVED_TIME, 50);
1490        alert.put(Calendar.CalendarAlerts.NOTIFY_TIME, 60);
1491        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.SCHEDULED);
1492        alert.put(Calendar.CalendarAlerts.MINUTES, 30);
1493        alert.put(Calendar.CalendarAlerts.EVENT_ID, eventId);
1494
1495        Uri alertUri = mResolver.insert(
1496                updatedUri(Calendar.CalendarAlerts.CONTENT_URI, syncAdapter), alert);
1497        // Alerts don't dirty the event
1498        testAndClearDirty(eventId, 0);
1499        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
1500
1501        ContentValues extended = new ContentValues();
1502        extended.put(Calendar.ExtendedProperties.NAME, "foo");
1503        extended.put(Calendar.ExtendedProperties.VALUE, "bar");
1504        extended.put(Calendar.ExtendedProperties.EVENT_ID, eventId);
1505
1506        Uri extendedUri = mResolver.insert(
1507                updatedUri(Calendar.ExtendedProperties.CONTENT_URI, syncAdapter), extended);
1508        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1509        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
1510
1511        // Now test updates
1512
1513        attendee = new ContentValues();
1514        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Sam");
1515        // Need to include EVENT_ID with attendee update.  Is that desired?
1516        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1517
1518        assertEquals("update", 1, mResolver.update(updatedUri(attendeeUri, syncAdapter), attendee,
1519                null /* where */, null /* selectionArgs */));
1520        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1521
1522        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
1523
1524        reminder = new ContentValues();
1525        reminder.put(Calendar.Reminders.MINUTES, 20);
1526
1527        assertEquals("update", 1, mResolver.update(updatedUri(reminderUri, syncAdapter), reminder,
1528                null /* where */, null /* selectionArgs */));
1529        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1530        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
1531
1532        alert = new ContentValues();
1533        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.DISMISSED);
1534
1535        assertEquals("update", 1, mResolver.update(updatedUri(alertUri, syncAdapter), alert,
1536                null /* where */, null /* selectionArgs */));
1537        // Alerts don't dirty the event
1538        testAndClearDirty(eventId, 0);
1539        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
1540
1541        extended = new ContentValues();
1542        extended.put(Calendar.ExtendedProperties.VALUE, "baz");
1543
1544        assertEquals("update", 1, mResolver.update(updatedUri(extendedUri, syncAdapter), extended,
1545                null /* where */, null /* selectionArgs */));
1546        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1547        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
1548
1549        // Now test deletes
1550
1551        assertEquals("delete", 1, mResolver.delete(
1552                updatedUri(attendeeUri, syncAdapter),
1553                null, null /* selectionArgs */));
1554        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1555        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
1556
1557        assertEquals("delete", 1, mResolver.delete(updatedUri(reminderUri, syncAdapter),
1558                null /* where */, null /* selectionArgs */));
1559
1560        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1561        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
1562
1563        assertEquals("delete", 1, mResolver.delete(updatedUri(alertUri, syncAdapter),
1564                null /* where */, null /* selectionArgs */));
1565
1566        // Alerts don't dirty the event
1567        testAndClearDirty(eventId, 0);
1568        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
1569
1570        assertEquals("delete", 1, mResolver.delete(updatedUri(extendedUri, syncAdapter),
1571                null /* where */, null /* selectionArgs */));
1572
1573        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1574        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 0);
1575    }
1576
1577    /**
1578     * Test calendar deletion
1579     * @throws Exception
1580     */
1581    public void testCalendarDeletion() throws Exception {
1582        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1583        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1584        long eventId = ContentUris.parseId(eventUri);
1585        testAndClearDirty(eventId, 1);
1586        Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1"));
1587        long eventId1 = ContentUris.parseId(eventUri);
1588        assertEquals("delete", 1, mResolver.delete(eventUri1, null, null));
1589        // Calendar has one event and one deleted event
1590        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1591
1592        assertEquals("delete", 1, mResolver.delete(Calendar.Calendars.CONTENT_URI,
1593                "_id=" + mCalendarId, null));
1594        // Calendar should be deleted
1595        testQueryCount(Calendar.Calendars.CONTENT_URI, null, 0);
1596        // Event should be gone
1597        testQueryCount(Calendar.Events.CONTENT_URI, null, 0);
1598    }
1599
1600    /**
1601     * Test multiple account support.
1602     */
1603    public void testMultipleAccounts() throws Exception {
1604        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1605        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1606        Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0"));
1607        Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"));
1608
1609        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1610        Uri eventsWithAccount = Calendar.Events.CONTENT_URI.buildUpon()
1611                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_NAME, "joe@joe.com")
1612                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_TYPE, "com.google")
1613                .build();
1614        // Only one event for that account
1615        testQueryCount(eventsWithAccount, null, 1);
1616
1617        // Test deletion with account and selection
1618
1619        long eventId = ContentUris.parseId(eventUri1);
1620        // Wrong account, should not be deleted
1621        assertEquals("delete", 0, mResolver.delete(
1622                updatedUri(eventsWithAccount, true /* syncAdapter */),
1623                "_id=" + eventId, null /* selectionArgs */));
1624        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1625        // Right account, should be deleted
1626        assertEquals("delete", 1, mResolver.delete(
1627                updatedUri(Calendar.Events.CONTENT_URI, true /* syncAdapter */),
1628                "_id=" + eventId, null /* selectionArgs */));
1629        testQueryCount(Calendar.Events.CONTENT_URI, null, 1);
1630    }
1631
1632    /**
1633     * Run commands, wiping instance table at each step.
1634     * This tests full instance expansion.
1635     * @throws Exception
1636     */
1637    public void testCommandSequences1() throws Exception {
1638        commandSequences(true);
1639    }
1640
1641    /**
1642     * Run commands normally.
1643     * This tests incremental instance expansion.
1644     * @throws Exception
1645     */
1646    public void testCommandSequences2() throws Exception {
1647        commandSequences(false);
1648    }
1649
1650    /**
1651     * Run thorough set of command sequences
1652     * @param wipe true if instances should be wiped and regenerated
1653     * @throws Exception
1654     */
1655    private void commandSequences(boolean wipe) throws Exception {
1656        Cursor cursor;
1657        Uri url = null;
1658        mWipe = wipe; // Set global flag
1659
1660        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1661
1662        cursor = mResolver.query(mEventsUri, null, null, null, null);
1663        assertEquals(0, cursor.getCount());
1664        cursor.close();
1665        Command[] commands;
1666
1667        Log.i(TAG, "Normal insert/delete");
1668        commands = mNormalInsertDelete;
1669        for (Command command : commands) {
1670            command.execute();
1671        }
1672
1673        deleteAllEvents();
1674
1675        Log.i(TAG, "All-day insert/delete");
1676        commands = mAlldayInsertDelete;
1677        for (Command command : commands) {
1678            command.execute();
1679        }
1680
1681        deleteAllEvents();
1682
1683        Log.i(TAG, "Recurring insert/delete");
1684        commands = mRecurringInsertDelete;
1685        for (Command command : commands) {
1686            command.execute();
1687        }
1688
1689        deleteAllEvents();
1690
1691        Log.i(TAG, "Exception with truncated recurrence");
1692        commands = mExceptionWithTruncatedRecurrence;
1693        for (Command command : commands) {
1694            command.execute();
1695        }
1696
1697        deleteAllEvents();
1698
1699        Log.i(TAG, "Exception with moved recurrence");
1700        commands = mExceptionWithMovedRecurrence;
1701        for (Command command : commands) {
1702            command.execute();
1703        }
1704
1705        deleteAllEvents();
1706
1707        Log.i(TAG, "Exception with cancel");
1708        commands = mCancelInstance;
1709        for (Command command : commands) {
1710            command.execute();
1711        }
1712
1713        deleteAllEvents();
1714
1715        Log.i(TAG, "Exception with moved recurrence2");
1716        commands = mExceptionWithMovedRecurrence2;
1717        for (Command command : commands) {
1718            command.execute();
1719        }
1720
1721        deleteAllEvents();
1722
1723        Log.i(TAG, "Exception with no recurrence");
1724        commands = mExceptionWithNoRecurrence;
1725        for (Command command : commands) {
1726            command.execute();
1727        }
1728    }
1729
1730    /**
1731     * Test Time toString.
1732     * @throws Exception
1733     */
1734    // Suppressed because toString currently hangs.
1735    @Suppress
1736    public void testTimeToString() throws Exception {
1737        Time time = new Time(Time.TIMEZONE_UTC);
1738        String str = "2039-01-01T23:00:00.000Z";
1739        String result = "20390101T230000UTC(0,0,0,-1,0)";
1740        time.parse3339(str);
1741        assertEquals(result, time.toString());
1742    }
1743
1744    /**
1745     * Test the query done by Event.loadEvents
1746     * @throws Exception
1747     */
1748    public void testInstanceQuery() throws Exception {
1749        final String[] PROJECTION = new String[] {
1750                Instances.TITLE,                 // 0
1751                Instances.EVENT_LOCATION,        // 1
1752                Instances.ALL_DAY,               // 2
1753                Instances.COLOR,                 // 3
1754                Instances.EVENT_TIMEZONE,        // 4
1755                Instances.EVENT_ID,              // 5
1756                Instances.BEGIN,                 // 6
1757                Instances.END,                   // 7
1758                Instances._ID,                   // 8
1759                Instances.START_DAY,             // 9
1760                Instances.END_DAY,               // 10
1761                Instances.START_MINUTE,          // 11
1762                Instances.END_MINUTE,            // 12
1763                Instances.HAS_ALARM,             // 13
1764                Instances.RRULE,                 // 14
1765                Instances.RDATE,                 // 15
1766                Instances.SELF_ATTENDEE_STATUS,  // 16
1767                Events.ORGANIZER,                // 17
1768                Events.GUESTS_CAN_MODIFY,        // 18
1769        };
1770
1771        String orderBy = Instances.SORT_CALENDAR_VIEW;
1772        String where = Instances.SELF_ATTENDEE_STATUS + "!=" + Calendar.Attendees.ATTENDEE_STATUS_DECLINED;
1773
1774        long start = 0;
1775        long end = 0;
1776        Cursor c = Instances.query(mResolver, PROJECTION,
1777                start - DateUtils.DAY_IN_MILLIS, end + DateUtils.DAY_IN_MILLIS, where, orderBy);
1778        // Just make sure the query doesn't crash.  TODO: could check results
1779    }
1780
1781    private Cursor queryInstances(long begin, long end) {
1782        Uri url = Uri.withAppendedPath(Calendar.Instances.CONTENT_URI, begin + "/" + end);
1783        return mResolver.query(url, null, null, null, null);
1784    }
1785
1786    protected static class MockProvider extends ContentProvider {
1787
1788        private String mAuthority;
1789
1790        private int mNumItems = 0;
1791
1792        public MockProvider(String authority) {
1793            mAuthority = authority;
1794        }
1795
1796        @Override
1797        public boolean onCreate() {
1798            return true;
1799        }
1800
1801        @Override
1802        public Cursor query(Uri uri, String[] projection, String selection,
1803                String[] selectionArgs, String sortOrder) {
1804            return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>());
1805        }
1806
1807        @Override
1808        public String getType(Uri uri) {
1809            throw new UnsupportedOperationException();
1810        }
1811
1812        @Override
1813        public Uri insert(Uri uri, ContentValues values) {
1814            mNumItems++;
1815            return Uri.parse("content://" + mAuthority + "/" + mNumItems);
1816        }
1817
1818        @Override
1819        public int delete(Uri uri, String selection, String[] selectionArgs) {
1820            return 0;
1821        }
1822
1823        @Override
1824        public int update(Uri uri, ContentValues values, String selection,
1825                String[] selectionArgs) {
1826            return 0;
1827        }
1828    }
1829}
1830