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