CalendarProvider2Test.java revision 263a8c5f662a16b23d0feb7a9ae2709093b34cbb
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, "joe@joe.com");
884        m.put(Calendars._SYNC_ACCOUNT,  account);
885        m.put(Calendars._SYNC_ACCOUNT_TYPE,  "com.google");
886
887        Uri url = mResolver.insert(Uri.parse("content://calendar/calendars"), m);
888        String id = url.getLastPathSegment();
889        return Integer.parseInt(id);
890    }
891
892    private Uri insertEvent(int calId, EventInfo event) {
893        return insertEvent(calId, event, "joe@joe.com");
894    }
895
896    private Uri insertEvent(int calId, EventInfo event, String account) {
897        if (mWipe) {
898            // Wipe instance table so it will be regenerated
899            mMetaData.clearInstanceRange();
900        }
901        ContentValues m = new ContentValues();
902        m.put(Events.CALENDAR_ID, calId);
903        m.put(Events.TITLE, event.mTitle);
904        m.put(Events.DTSTART, event.mDtstart);
905        m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
906        m.put(Calendars._SYNC_ACCOUNT,  account);
907        m.put(Calendars._SYNC_ACCOUNT_TYPE,  "com.google");
908
909        if (event.mRrule == null) {
910            // This is a normal event
911            m.put(Events.DTEND, event.mDtend);
912        } else {
913            // This is a repeating event
914            m.put(Events.RRULE, event.mRrule);
915            m.put(Events.DURATION, event.mDuration);
916        }
917
918        if (event.mDescription != null) {
919            m.put(Events.DESCRIPTION, event.mDescription);
920        }
921        if (event.mTimezone != null) {
922            m.put(Events.EVENT_TIMEZONE, event.mTimezone);
923        }
924
925        if (event.mOriginalTitle != null) {
926            // This is a recurrence exception.
927            EventInfo recur = findEvent(event.mOriginalTitle);
928            assertNotNull(recur);
929            String syncId = String.format("%d", recur.mSyncId);
930            m.put(Events.ORIGINAL_EVENT, syncId);
931            m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0);
932            m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
933        }
934        Uri url = mResolver.insert(mEventsUri, m);
935
936        // Create a fake _sync_id and add it to the event.  Update the database
937        // directly so that we don't trigger any validation checks in the
938        // CalendarProvider.
939        long id = ContentUris.parseId(url);
940        mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id);
941        event.mSyncId = mGlobalSyncId;
942        mGlobalSyncId += 1;
943
944        return url;
945    }
946
947    /**
948     * Deletes all the events that match the given title.
949     * @param title the given title to match events on
950     * @return the number of rows deleted
951     */
952    private int deleteMatchingEvents(String title) {
953        Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID },
954                "title=?", new String[] { title }, null);
955        int numRows = 0;
956        while (cursor.moveToNext()) {
957            long id = cursor.getLong(0);
958            // Do delete as a sync adapter so event is really deleted, not just marked
959            // as deleted.
960            Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true);
961            numRows += mResolver.delete(uri, null, null);
962        }
963        cursor.close();
964        return numRows;
965    }
966
967    /**
968     * Updates all the events that match the given title.
969     * @param title the given title to match events on
970     * @return the number of rows updated
971     */
972    private int updateMatchingEvents(String title, ContentValues values) {
973        String[] projection = new String[] {
974                Events._ID,
975                Events.DTSTART,
976                Events.DTEND,
977                Events.DURATION,
978                Events.ALL_DAY,
979                Events.RRULE,
980                Events.EVENT_TIMEZONE,
981                Events.ORIGINAL_EVENT,
982        };
983        Cursor cursor = mResolver.query(mEventsUri, projection,
984                "title=?", new String[] { title }, null);
985        int numRows = 0;
986        while (cursor.moveToNext()) {
987            long id = cursor.getLong(0);
988
989            // If any of the following fields are being changed, then we need
990            // to include all of them.
991            if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
992                    || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
993                    || values.containsKey(Events.RRULE)
994                    || values.containsKey(Events.EVENT_TIMEZONE)
995                    || values.containsKey(Calendar.EventsColumns.STATUS)) {
996                long dtstart = cursor.getLong(1);
997                long dtend = cursor.getLong(2);
998                String duration = cursor.getString(3);
999                boolean allDay = cursor.getInt(4) != 0;
1000                String rrule = cursor.getString(5);
1001                String timezone = cursor.getString(6);
1002                String originalEvent = cursor.getString(7);
1003
1004                if (!values.containsKey(Events.DTSTART)) {
1005                    values.put(Events.DTSTART, dtstart);
1006                }
1007                // Don't add DTEND for repeating events
1008                if (!values.containsKey(Events.DTEND) && rrule == null) {
1009                    values.put(Events.DTEND, dtend);
1010                }
1011                if (!values.containsKey(Events.DURATION) && duration != null) {
1012                    values.put(Events.DURATION, duration);
1013                }
1014                if (!values.containsKey(Events.ALL_DAY)) {
1015                    values.put(Events.ALL_DAY, allDay ? 1 : 0);
1016                }
1017                if (!values.containsKey(Events.RRULE) && rrule != null) {
1018                    values.put(Events.RRULE, rrule);
1019                }
1020                if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
1021                    values.put(Events.EVENT_TIMEZONE, timezone);
1022                }
1023                if (!values.containsKey(Events.ORIGINAL_EVENT) && originalEvent != null) {
1024                    values.put(Events.ORIGINAL_EVENT, originalEvent);
1025                }
1026            }
1027
1028            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1029            numRows += mResolver.update(uri, values, null, null);
1030        }
1031        cursor.close();
1032        return numRows;
1033    }
1034
1035    private void deleteAllEvents() {
1036        mDb.execSQL("DELETE FROM Events;");
1037        mMetaData.clearInstanceRange();
1038    }
1039
1040    public void testInsertNormalEvents() throws Exception {
1041        Cursor cursor;
1042        Uri url = null;
1043
1044        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1045
1046        cursor = mResolver.query(mEventsUri, null, null, null, null);
1047        assertEquals(0, cursor.getCount());
1048        cursor.close();
1049
1050        // Keep track of the number of normal events
1051        int numEvents = 0;
1052
1053        // "begin" is the earliest start time of all the normal events,
1054        // and "end" is the latest end time of all the normal events.
1055        long begin = 0, end = 0;
1056
1057        int len = mEvents.length;
1058        for (int ii = 0; ii < len; ii++) {
1059            EventInfo event = mEvents[ii];
1060            // Skip repeating events and recurrence exceptions
1061            if (event.mRrule != null || event.mOriginalTitle != null) {
1062                continue;
1063            }
1064            if (numEvents == 0) {
1065                begin = event.mDtstart;
1066                end = event.mDtend;
1067            } else {
1068                if (begin > event.mDtstart) {
1069                    begin = event.mDtstart;
1070                }
1071                if (end < event.mDtend) {
1072                    end = event.mDtend;
1073                }
1074            }
1075            url = insertEvent(calId, event);
1076            numEvents += 1;
1077        }
1078
1079        // query one
1080        cursor = mResolver.query(url, null, null, null, null);
1081        assertEquals(1, cursor.getCount());
1082        cursor.close();
1083
1084        // query all
1085        cursor = mResolver.query(mEventsUri, null, null, null, null);
1086        assertEquals(numEvents, cursor.getCount());
1087        cursor.close();
1088
1089        // Check that the Instances table has one instance of each of the
1090        // normal events.
1091        cursor = queryInstances(begin, end);
1092        assertEquals(numEvents, cursor.getCount());
1093        cursor.close();
1094    }
1095
1096    public void testInsertRepeatingEvents() throws Exception {
1097        Cursor cursor;
1098        Uri url = null;
1099
1100        int calId = insertCal("Calendar0", "America/Los_Angeles");
1101
1102        cursor = mResolver.query(mEventsUri, null, null, null, null);
1103        assertEquals(0, cursor.getCount());
1104        cursor.close();
1105
1106        // Keep track of the number of repeating events
1107        int numEvents = 0;
1108
1109        int len = mEvents.length;
1110        for (int ii = 0; ii < len; ii++) {
1111            EventInfo event = mEvents[ii];
1112            // Skip normal events
1113            if (event.mRrule == null) {
1114                continue;
1115            }
1116            url = insertEvent(calId, event);
1117            numEvents += 1;
1118        }
1119
1120        // query one
1121        cursor = mResolver.query(url, null, null, null, null);
1122        assertEquals(1, cursor.getCount());
1123        cursor.close();
1124
1125        // query all
1126        cursor = mResolver.query(mEventsUri, null, null, null, null);
1127        assertEquals(numEvents, cursor.getCount());
1128        cursor.close();
1129    }
1130
1131    public void testInstanceRange() throws Exception {
1132        Cursor cursor;
1133        Uri url = null;
1134
1135        int calId = insertCal("Calendar0", "America/Los_Angeles");
1136
1137        cursor = mResolver.query(mEventsUri, null, null, null, null);
1138        assertEquals(0, cursor.getCount());
1139        cursor.close();
1140
1141        int len = mInstanceRanges.length;
1142        for (int ii = 0; ii < len; ii++) {
1143            InstanceInfo instance = mInstanceRanges[ii];
1144            EventInfo event = instance.mEvent;
1145            url = insertEvent(calId, event);
1146            cursor = queryInstances(instance.mBegin, instance.mEnd);
1147            if (instance.mExpectedOccurrences != cursor.getCount()) {
1148                Log.e(TAG, "Test failed! Instance index: " + ii);
1149                Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription
1150                        + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]"
1151                        + " expected: " + instance.mExpectedOccurrences);
1152                dumpCursor(cursor);
1153            }
1154            assertEquals(instance.mExpectedOccurrences, cursor.getCount());
1155            cursor.close();
1156            // Delete as sync_adapter so event is really deleted.
1157            int rows = mResolver.delete(updatedUri(url, true),
1158                    null /* selection */, null /* selection args */);
1159            assertEquals(1, rows);
1160        }
1161    }
1162
1163    public void testEntityQuery() throws Exception {
1164        testInsertNormalEvents(); // To initialize
1165
1166        ContentValues reminder = new ContentValues();
1167        reminder.put(Calendar.Reminders.EVENT_ID, 1);
1168        reminder.put(Calendar.Reminders.MINUTES, 10);
1169        reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_SMS);
1170        mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
1171        reminder.put(Calendar.Reminders.MINUTES, 20);
1172        mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
1173
1174        ContentValues extended = new ContentValues();
1175        extended.put(Calendar.ExtendedProperties.NAME, "foo");
1176        extended.put(Calendar.ExtendedProperties.VALUE, "bar");
1177        extended.put(Calendar.ExtendedProperties.EVENT_ID, 2);
1178        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1179        extended.put(Calendar.ExtendedProperties.EVENT_ID, 1);
1180        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1181        extended.put(Calendar.ExtendedProperties.NAME, "foo2");
1182        extended.put(Calendar.ExtendedProperties.VALUE, "bar2");
1183        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1184
1185        ContentValues attendee = new ContentValues();
1186        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1187        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1188        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1189                Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
1190        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1191        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1192                Calendar.Attendees.RELATIONSHIP_PERFORMER);
1193        attendee.put(Calendar.Attendees.EVENT_ID, 3);
1194        mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1195
1196        EntityIterator ei = EventsEntity.newEntityIterator(
1197                mResolver.query(EventsEntity.CONTENT_URI, null, null, null, null), mResolver);
1198        int count = 0;
1199        try {
1200            while (ei.hasNext()) {
1201                Entity entity = ei.next();
1202                ContentValues values = entity.getEntityValues();
1203                assertEquals(CALENDAR_URL, values.getAsString(Calendars.URL));
1204                ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
1205                switch (values.getAsInteger("_id")) {
1206                    case 1:
1207                        assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties
1208                        break;
1209                    case 2:
1210                        assertEquals(1, subvalues.size()); // Extended properties
1211                        break;
1212                    case 3:
1213                        assertEquals(1, subvalues.size()); // Attendees
1214                        break;
1215                    default:
1216                        assertEquals(0, subvalues.size());
1217                        break;
1218                }
1219                count += 1;
1220            }
1221            assertEquals(5, count);
1222        } finally {
1223            ei.close();
1224        }
1225
1226        ei = EventsEntity.newEntityIterator(
1227                    mResolver.query(EventsEntity.CONTENT_URI, null, "_id = 3", null, null),
1228                mResolver);
1229        try {
1230            count = 0;
1231            while (ei.hasNext()) {
1232                Entity entity = ei.next();
1233                count += 1;
1234            }
1235            assertEquals(1, count);
1236        } finally {
1237            ei.close();
1238        }
1239    }
1240
1241    /**
1242     * Test attendee processing
1243     * @throws Exception
1244     */
1245    public void testAttendees() throws Exception {
1246        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1247
1248        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1249        long eventId = ContentUris.parseId(eventUri);
1250
1251        ContentValues attendee = new ContentValues();
1252        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1253        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1254        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1255        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1256                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
1257        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1258        Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1259
1260        Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1261                "event_id=" + eventId, null, null);
1262        assertEquals(1, cursor.getCount());
1263        cursor.close();
1264
1265        cursor = mResolver.query(eventUri, null, null, null, null);
1266        int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS);
1267        cursor.moveToNext();
1268        long selfAttendeeStatus = cursor.getInt(selfColumn);
1269        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
1270        cursor.close();
1271
1272        // Change status to declined
1273        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1274                Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
1275        mResolver.update(attendeesUri, attendee, null, null);
1276
1277        cursor = mResolver.query(eventUri, null, null, null, null);
1278        cursor.moveToNext();
1279        selfAttendeeStatus = cursor.getInt(selfColumn);
1280        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1281        cursor.close();
1282
1283        // Add another attendee
1284        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude");
1285        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
1286        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1287                Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED);
1288        mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1289
1290        cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1291                "event_id=" + mCalendarId, null, null);
1292        assertEquals(2, cursor.getCount());
1293        cursor.close();
1294
1295        cursor = mResolver.query(eventUri, null, null, null, null);
1296        cursor.moveToNext();
1297        selfAttendeeStatus = cursor.getInt(selfColumn);
1298        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1299        cursor.close();
1300    }
1301
1302    /**
1303     * Test the event's _sync_dirty status and clear it.
1304     * @param eventId event to fetch.
1305     * @param wanted the wanted _sync_dirty status
1306     */
1307    private void testAndClearDirty(long eventId, int wanted) {
1308        Cursor cursor = mResolver.query(
1309                ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId),
1310                null, null, null, null);
1311        try {
1312            assertEquals("Event count", 1, cursor.getCount());
1313            cursor.moveToNext();
1314            int dirty = cursor.getInt(cursor.getColumnIndex(Calendar.Events._SYNC_DIRTY));
1315            assertEquals("dirty flag", wanted, dirty);
1316            if (dirty == 1) {
1317                // Have to access database directly since provider will set dirty again.
1318                mDb.execSQL("UPDATE Events SET _sync_dirty=0 WHERE _id=" + eventId);
1319            }
1320        } finally {
1321            cursor.close();
1322        }
1323    }
1324
1325    /**
1326     * Test the count of results from a query.
1327     * @param uri The URI to query
1328     * @param where The where string or null.
1329     * @param wanted The number of results wanted.  An assertion is thrown if it doesn't match.
1330     */
1331    private void testQueryCount(Uri uri, String where, int wanted) {
1332        Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */,
1333                null /* sortOrder */);
1334        try {
1335            dumpCursor(cursor);
1336            assertEquals("query results", wanted, cursor.getCount());
1337        } finally {
1338            cursor.close();
1339        }
1340    }
1341
1342    /**
1343     * Test dirty flag processing.
1344     * @throws Exception
1345     */
1346    public void testDirty() throws Exception {
1347        internalTestDirty(false);
1348    }
1349
1350    /**
1351     * Test dirty flag processing for updates from a sync adapter.
1352     * @throws Exception
1353     */
1354    public void testDirtyWithSyncAdapter() throws Exception {
1355        internalTestDirty(true);
1356    }
1357
1358    /**
1359     * Add CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.
1360     */
1361    private Uri updatedUri(Uri uri, boolean syncAdapter) {
1362        if (syncAdapter) {
1363            return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
1364                    .build();
1365        } else {
1366            return uri;
1367        }
1368    }
1369
1370    /**
1371     * Test dirty flag processing either for syncAdapter operations or client operations.
1372     * The main difference is syncAdapter operations don't set the dirty bit.
1373     */
1374    private void internalTestDirty(boolean syncAdapter) throws Exception {
1375        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1376
1377        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1378
1379        long eventId = ContentUris.parseId(eventUri);
1380        testAndClearDirty(eventId, 1);
1381
1382        ContentValues attendee = new ContentValues();
1383        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1384        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1385        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1386        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1387                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
1388        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1389
1390        Uri attendeeUri = mResolver.insert(
1391                updatedUri(Calendar.Attendees.CONTENT_URI, syncAdapter),
1392                attendee);
1393        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1394        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
1395
1396        ContentValues reminder = new ContentValues();
1397        reminder.put(Calendar.Reminders.MINUTES, 10);
1398        reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_EMAIL);
1399        reminder.put(Calendar.Attendees.EVENT_ID, eventId);
1400
1401        Uri reminderUri = mResolver.insert(
1402                updatedUri(Calendar.Reminders.CONTENT_URI, syncAdapter), reminder);
1403        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1404        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
1405
1406        ContentValues alert = new ContentValues();
1407        alert.put(Calendar.CalendarAlerts.BEGIN, 10);
1408        alert.put(Calendar.CalendarAlerts.END, 20);
1409        alert.put(Calendar.CalendarAlerts.ALARM_TIME, 30);
1410        alert.put(Calendar.CalendarAlerts.CREATION_TIME, 40);
1411        alert.put(Calendar.CalendarAlerts.RECEIVED_TIME, 50);
1412        alert.put(Calendar.CalendarAlerts.NOTIFY_TIME, 60);
1413        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.SCHEDULED);
1414        alert.put(Calendar.CalendarAlerts.MINUTES, 30);
1415        alert.put(Calendar.CalendarAlerts.EVENT_ID, eventId);
1416
1417        Uri alertUri = mResolver.insert(
1418                updatedUri(Calendar.CalendarAlerts.CONTENT_URI, syncAdapter), alert);
1419        // Alerts don't dirty the event
1420        testAndClearDirty(eventId, 0);
1421        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
1422
1423        ContentValues extended = new ContentValues();
1424        extended.put(Calendar.ExtendedProperties.NAME, "foo");
1425        extended.put(Calendar.ExtendedProperties.VALUE, "bar");
1426        extended.put(Calendar.ExtendedProperties.EVENT_ID, eventId);
1427
1428        Uri extendedUri = mResolver.insert(
1429                updatedUri(Calendar.ExtendedProperties.CONTENT_URI, syncAdapter), extended);
1430        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1431        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
1432
1433        // Now test updates
1434
1435        attendee = new ContentValues();
1436        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Sam");
1437        // Need to include EVENT_ID with attendee update.  Is that desired?
1438        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1439
1440        assertEquals("update", 1, mResolver.update(updatedUri(attendeeUri, syncAdapter), attendee,
1441                null /* where */, null /* selectionArgs */));
1442        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1443
1444        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
1445
1446        reminder = new ContentValues();
1447        reminder.put(Calendar.Reminders.MINUTES, 20);
1448
1449        assertEquals("update", 1, mResolver.update(updatedUri(reminderUri, syncAdapter), reminder,
1450                null /* where */, null /* selectionArgs */));
1451        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1452        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
1453
1454        alert = new ContentValues();
1455        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.DISMISSED);
1456
1457        assertEquals("update", 1, mResolver.update(updatedUri(alertUri, syncAdapter), alert,
1458                null /* where */, null /* selectionArgs */));
1459        // Alerts don't dirty the event
1460        testAndClearDirty(eventId, 0);
1461        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
1462
1463        extended = new ContentValues();
1464        extended.put(Calendar.ExtendedProperties.VALUE, "baz");
1465
1466        assertEquals("update", 1, mResolver.update(updatedUri(extendedUri, syncAdapter), extended,
1467                null /* where */, null /* selectionArgs */));
1468        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1469        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
1470
1471        // Now test deletes
1472
1473        assertEquals("delete", 1, mResolver.delete(
1474                updatedUri(attendeeUri, syncAdapter),
1475                null, null /* selectionArgs */));
1476        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1477        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
1478
1479        assertEquals("delete", 1, mResolver.delete(updatedUri(reminderUri, syncAdapter),
1480                null /* where */, null /* selectionArgs */));
1481
1482        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1483        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
1484
1485        assertEquals("delete", 1, mResolver.delete(updatedUri(alertUri, syncAdapter),
1486                null /* where */, null /* selectionArgs */));
1487
1488        // Alerts don't dirty the event
1489        testAndClearDirty(eventId, 0);
1490        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
1491
1492        assertEquals("delete", 1, mResolver.delete(updatedUri(extendedUri, syncAdapter),
1493                null /* where */, null /* selectionArgs */));
1494
1495        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1496        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 0);
1497    }
1498
1499    /**
1500     * Test calendar deletion
1501     * @throws Exception
1502     */
1503    public void testCalendarDeletion() throws Exception {
1504        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1505        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1506        long eventId = ContentUris.parseId(eventUri);
1507        testAndClearDirty(eventId, 1);
1508        Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1"));
1509        long eventId1 = ContentUris.parseId(eventUri);
1510        assertEquals("delete", 1, mResolver.delete(eventUri1, null, null));
1511        // Calendar has one event and one deleted event
1512        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1513
1514        assertEquals("delete", 1, mResolver.delete(Calendar.Calendars.CONTENT_URI,
1515                "_id=" + mCalendarId, null));
1516        // Calendar should be deleted
1517        testQueryCount(Calendar.Calendars.CONTENT_URI, null, 0);
1518        // Event should be gone
1519        testQueryCount(Calendar.Events.CONTENT_URI, null, 0);
1520    }
1521
1522    /**
1523     * Test multiple account support.
1524     */
1525    public void testMultipleAccounts() throws Exception {
1526        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1527        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1528        Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0"));
1529        Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"), "user2@google.com");
1530
1531        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1532        Uri eventsWithAccount = Calendar.Events.CONTENT_URI.buildUpon()
1533                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_NAME, "joe@joe.com")
1534                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_TYPE, "com.google")
1535                .build();
1536        // Only one event for that account
1537        testQueryCount(eventsWithAccount, null, 1);
1538
1539        // Test deletion with account and selection
1540
1541        long eventId = ContentUris.parseId(eventUri1);
1542        // Wrong account, should not be deleted
1543        assertEquals("delete", 0, mResolver.delete(
1544                updatedUri(eventsWithAccount, true /* syncAdapter */),
1545                "_id=" + eventId, null /* selectionArgs */));
1546        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1547        // Right account, should be deleted
1548        assertEquals("delete", 1, mResolver.delete(
1549                updatedUri(Calendar.Events.CONTENT_URI, true /* syncAdapter */),
1550                "_id=" + eventId, null /* selectionArgs */));
1551        testQueryCount(Calendar.Events.CONTENT_URI, null, 1);
1552    }
1553
1554    /**
1555     * Run commands, wiping instance table at each step.
1556     * This tests full instance expansion.
1557     * @throws Exception
1558     */
1559    public void testCommandSequences1() throws Exception {
1560        commandSequences(true);
1561    }
1562
1563    /**
1564     * Run commands normally.
1565     * This tests incremental instance expansion.
1566     * @throws Exception
1567     */
1568    public void testCommandSequences2() throws Exception {
1569        commandSequences(false);
1570    }
1571
1572    /**
1573     * Run thorough set of command sequences
1574     * @param wipe true if instances should be wiped and regenerated
1575     * @throws Exception
1576     */
1577    private void commandSequences(boolean wipe) throws Exception {
1578        Cursor cursor;
1579        Uri url = null;
1580        mWipe = wipe; // Set global flag
1581
1582        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1583
1584        cursor = mResolver.query(mEventsUri, null, null, null, null);
1585        dumpCursor(cursor);
1586        assertEquals(0, cursor.getCount());
1587        cursor.close();
1588        Command[] commands;
1589
1590        Log.i(TAG, "Normal insert/delete");
1591        commands = mNormalInsertDelete;
1592        for (Command command : commands) {
1593            command.execute();
1594        }
1595
1596        deleteAllEvents();
1597
1598        Log.i(TAG, "All-day insert/delete");
1599        commands = mAlldayInsertDelete;
1600        for (Command command : commands) {
1601            command.execute();
1602        }
1603
1604        deleteAllEvents();
1605
1606        Log.i(TAG, "Recurring insert/delete");
1607        commands = mRecurringInsertDelete;
1608        for (Command command : commands) {
1609            command.execute();
1610        }
1611
1612        deleteAllEvents();
1613
1614        Log.i(TAG, "Exception with truncated recurrence");
1615        commands = mExceptionWithTruncatedRecurrence;
1616        for (Command command : commands) {
1617            command.execute();
1618        }
1619
1620        deleteAllEvents();
1621
1622        Log.i(TAG, "Exception with moved recurrence");
1623        commands = mExceptionWithMovedRecurrence;
1624        for (Command command : commands) {
1625            command.execute();
1626        }
1627
1628        deleteAllEvents();
1629
1630        Log.i(TAG, "Exception with cancel");
1631        commands = mCancelInstance;
1632        for (Command command : commands) {
1633            command.execute();
1634        }
1635
1636        deleteAllEvents();
1637
1638        Log.i(TAG, "Exception with moved recurrence2");
1639        commands = mExceptionWithMovedRecurrence2;
1640        for (Command command : commands) {
1641            command.execute();
1642        }
1643
1644        deleteAllEvents();
1645
1646        Log.i(TAG, "Exception with no recurrence");
1647        commands = mExceptionWithNoRecurrence;
1648        for (Command command : commands) {
1649            command.execute();
1650        }
1651    }
1652
1653    /**
1654     * Test Time toString.
1655     * @throws Exception
1656     */
1657    // Suppressed because toString currently hangs.
1658    @Suppress
1659    public void testTimeToString() throws Exception {
1660        Time time = new Time(Time.TIMEZONE_UTC);
1661        String str = "2039-01-01T23:00:00.000Z";
1662        String result = "20390101T230000UTC(0,0,0,-1,0)";
1663        time.parse3339(str);
1664        assertEquals(result, time.toString());
1665    }
1666
1667    private Cursor queryInstances(long begin, long end) {
1668        Uri url = Uri.parse("content://calendar/instances/when/" + begin + "/" + end);
1669        return mResolver.query(url, null, null, null, null);
1670    }
1671
1672    protected static class MockProvider extends ContentProvider {
1673
1674        private String mAuthority;
1675
1676        private int mNumItems = 0;
1677
1678        public MockProvider(String authority) {
1679            mAuthority = authority;
1680        }
1681
1682        @Override
1683        public boolean onCreate() {
1684            return true;
1685        }
1686
1687        @Override
1688        public Cursor query(Uri uri, String[] projection, String selection,
1689                String[] selectionArgs, String sortOrder) {
1690            return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>());
1691        }
1692
1693        @Override
1694        public String getType(Uri uri) {
1695            throw new UnsupportedOperationException();
1696        }
1697
1698        @Override
1699        public Uri insert(Uri uri, ContentValues values) {
1700            mNumItems++;
1701            return Uri.parse("content://" + mAuthority + "/" + mNumItems);
1702        }
1703
1704        @Override
1705        public int delete(Uri uri, String selection, String[] selectionArgs) {
1706            return 0;
1707        }
1708
1709        @Override
1710        public int update(Uri uri, ContentValues values, String selection,
1711                String[] selectionArgs) {
1712            return 0;
1713        }
1714    }
1715}
1716