CalendarProvider2Test.java revision 68040cf2602bb15bb4cf1072cf99132118d5a805
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(4, subvalues.size()); // 2 x reminder, 2 x extended properties
1258                        break;
1259                    case 2:
1260                        assertEquals(1, subvalues.size()); // Extended properties
1261                        ContentValues subContentValues = subvalues.get(0).values;
1262                        String name = subContentValues.getAsString(
1263                                Calendar.ExtendedProperties.NAME);
1264                        String value = subContentValues.getAsString(
1265                                Calendar.ExtendedProperties.VALUE);
1266                        assertEquals("foo", name);
1267                        assertEquals("bar", value);
1268                        break;
1269                    case 3:
1270                        assertEquals(1, subvalues.size()); // Attendees
1271                        break;
1272                    default:
1273                        assertEquals(0, subvalues.size());
1274                        break;
1275                }
1276                count += 1;
1277            }
1278            assertEquals(5, count);
1279        } finally {
1280            ei.close();
1281        }
1282
1283        ei = EventsEntity.newEntityIterator(
1284                    mResolver.query(EventsEntity.CONTENT_URI, null, "_id = 3", null, null),
1285                mResolver);
1286        try {
1287            count = 0;
1288            while (ei.hasNext()) {
1289                Entity entity = ei.next();
1290                count += 1;
1291            }
1292            assertEquals(1, count);
1293        } finally {
1294            ei.close();
1295        }
1296    }
1297
1298    public void testDeleteCalendar() throws Exception {
1299        int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
1300        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1301        insertEvent(calendarId0, mEvents[0]);
1302        insertEvent(calendarId1, mEvents[1]);
1303        // Should have 2 calendars and 2 events
1304        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 2);
1305        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 2);
1306
1307        int deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI,
1308                "ownerAccount='user2@google.com'", null /* selectionArgs */);
1309
1310        assertEquals(1, deletes);
1311        // Should have 1 calendar and 1 event
1312        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 1);
1313        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 1);
1314
1315        deletes = mResolver.delete(Uri.withAppendedPath(Calendar.Calendars.CONTENT_URI,
1316                String.valueOf(calendarId0)),
1317                null /* selection*/ , null /* selectionArgs */);
1318
1319        assertEquals(1, deletes);
1320        // Should have 0 calendars and 0 events
1321        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 0);
1322        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 0);
1323
1324        deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI,
1325                "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */);
1326
1327        assertEquals(0, deletes);
1328    }
1329
1330    public void testCalendarAlerts() throws Exception {
1331        // This projection is from AlertActivity; want to make sure it works.
1332        String[] projection = new String[] {
1333                Calendar.CalendarAlerts._ID,              // 0
1334                Calendar.CalendarAlerts.TITLE,            // 1
1335                Calendar.CalendarAlerts.EVENT_LOCATION,   // 2
1336                Calendar.CalendarAlerts.ALL_DAY,          // 3
1337                Calendar.CalendarAlerts.BEGIN,            // 4
1338                Calendar.CalendarAlerts.END,              // 5
1339                Calendar.CalendarAlerts.EVENT_ID,         // 6
1340                Calendar.CalendarAlerts.COLOR,            // 7
1341                Calendar.CalendarAlerts.RRULE,            // 8
1342                Calendar.CalendarAlerts.HAS_ALARM,        // 9
1343                Calendar.CalendarAlerts.STATE,            // 10
1344                Calendar.CalendarAlerts.ALARM_TIME,       // 11
1345        };
1346        testInsertNormalEvents(); // To initialize
1347
1348        Uri alertUri = Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1349                2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
1350        Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1351                2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */);
1352
1353        // Regular query
1354        Cursor cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI, projection,
1355                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1356
1357        assertEquals(2, cursor.getCount());
1358        cursor.close();
1359
1360        // Instance query
1361        cursor = mResolver.query(alertUri, projection,
1362                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1363
1364        assertEquals(1, cursor.getCount());
1365        cursor.close();
1366
1367        // Grouped by event query
1368        cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI_BY_INSTANCE, projection,
1369                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1370
1371        assertEquals(1, cursor.getCount());
1372        cursor.close();
1373    }
1374
1375    /**
1376     * Test attendee processing
1377     * @throws Exception
1378     */
1379    public void testAttendees() throws Exception {
1380        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1381
1382        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1383        long eventId = ContentUris.parseId(eventUri);
1384
1385        ContentValues attendee = new ContentValues();
1386        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1387        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1388        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1389        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1390                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
1391        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1392        Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1393
1394        Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1395                "event_id=" + eventId, null, null);
1396        assertEquals("Created event is missing", 1, cursor.getCount());
1397        cursor.close();
1398
1399        cursor = mResolver.query(eventUri, null, null, null, null);
1400        assertEquals("Created event is missing", 1, cursor.getCount());
1401        int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS);
1402        cursor.moveToNext();
1403        long selfAttendeeStatus = cursor.getInt(selfColumn);
1404        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
1405        cursor.close();
1406
1407        // Change status to declined
1408        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1409                Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
1410        mResolver.update(attendeesUri, attendee, null, null);
1411
1412        cursor = mResolver.query(eventUri, null, null, null, null);
1413        cursor.moveToNext();
1414        selfAttendeeStatus = cursor.getInt(selfColumn);
1415        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1416        cursor.close();
1417
1418        // Add another attendee
1419        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude");
1420        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
1421        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1422                Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED);
1423        mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1424
1425        cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1426                "event_id=" + mCalendarId, null, null);
1427        assertEquals(2, cursor.getCount());
1428        cursor.close();
1429
1430        cursor = mResolver.query(eventUri, null, null, null, null);
1431        cursor.moveToNext();
1432        selfAttendeeStatus = cursor.getInt(selfColumn);
1433        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1434        cursor.close();
1435    }
1436
1437
1438    /**
1439     * Test the event's _sync_dirty status and clear it.
1440     * @param eventId event to fetch.
1441     * @param wanted the wanted _sync_dirty status
1442     */
1443    private void testAndClearDirty(long eventId, int wanted) {
1444        Cursor cursor = mResolver.query(
1445                ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId),
1446                null, null, null, null);
1447        try {
1448            assertEquals("Event count", 1, cursor.getCount());
1449            cursor.moveToNext();
1450            int dirty = cursor.getInt(cursor.getColumnIndex(Calendar.Events._SYNC_DIRTY));
1451            assertEquals("dirty flag", wanted, dirty);
1452            if (dirty == 1) {
1453                // Have to access database directly since provider will set dirty again.
1454                mDb.execSQL("UPDATE Events SET _sync_dirty=0 WHERE _id=" + eventId);
1455            }
1456        } finally {
1457            cursor.close();
1458        }
1459    }
1460
1461    /**
1462     * Test the count of results from a query.
1463     * @param uri The URI to query
1464     * @param where The where string or null.
1465     * @param wanted The number of results wanted.  An assertion is thrown if it doesn't match.
1466     */
1467    private void testQueryCount(Uri uri, String where, int wanted) {
1468        Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */,
1469                null /* sortOrder */);
1470        try {
1471            assertEquals("query results", wanted, cursor.getCount());
1472        } finally {
1473            cursor.close();
1474        }
1475    }
1476
1477    /**
1478     * Test dirty flag processing.
1479     * @throws Exception
1480     */
1481    public void testDirty() throws Exception {
1482        internalTestDirty(false);
1483    }
1484
1485    /**
1486     * Test dirty flag processing for updates from a sync adapter.
1487     * @throws Exception
1488     */
1489    public void testDirtyWithSyncAdapter() throws Exception {
1490        internalTestDirty(true);
1491    }
1492
1493    /**
1494     * Add CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.
1495     */
1496    private Uri updatedUri(Uri uri, boolean syncAdapter) {
1497        if (syncAdapter) {
1498            return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
1499                    .build();
1500        } else {
1501            return uri;
1502        }
1503    }
1504
1505    /**
1506     * Test dirty flag processing either for syncAdapter operations or client operations.
1507     * The main difference is syncAdapter operations don't set the dirty bit.
1508     */
1509    private void internalTestDirty(boolean syncAdapter) throws Exception {
1510        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1511
1512        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1513
1514        long eventId = ContentUris.parseId(eventUri);
1515        testAndClearDirty(eventId, 1);
1516
1517        ContentValues attendee = new ContentValues();
1518        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1519        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
1520        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1521        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1522                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
1523        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1524
1525        Uri attendeeUri = mResolver.insert(
1526                updatedUri(Calendar.Attendees.CONTENT_URI, syncAdapter),
1527                attendee);
1528        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1529        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
1530
1531        ContentValues reminder = new ContentValues();
1532        reminder.put(Calendar.Reminders.MINUTES, 10);
1533        reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_EMAIL);
1534        reminder.put(Calendar.Attendees.EVENT_ID, eventId);
1535
1536        Uri reminderUri = mResolver.insert(
1537                updatedUri(Calendar.Reminders.CONTENT_URI, syncAdapter), reminder);
1538        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1539        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
1540
1541        ContentValues alert = new ContentValues();
1542        alert.put(Calendar.CalendarAlerts.BEGIN, 10);
1543        alert.put(Calendar.CalendarAlerts.END, 20);
1544        alert.put(Calendar.CalendarAlerts.ALARM_TIME, 30);
1545        alert.put(Calendar.CalendarAlerts.CREATION_TIME, 40);
1546        alert.put(Calendar.CalendarAlerts.RECEIVED_TIME, 50);
1547        alert.put(Calendar.CalendarAlerts.NOTIFY_TIME, 60);
1548        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.SCHEDULED);
1549        alert.put(Calendar.CalendarAlerts.MINUTES, 30);
1550        alert.put(Calendar.CalendarAlerts.EVENT_ID, eventId);
1551
1552        Uri alertUri = mResolver.insert(
1553                updatedUri(Calendar.CalendarAlerts.CONTENT_URI, syncAdapter), alert);
1554        // Alerts don't dirty the event
1555        testAndClearDirty(eventId, 0);
1556        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
1557
1558        ContentValues extended = new ContentValues();
1559        extended.put(Calendar.ExtendedProperties.NAME, "foo");
1560        extended.put(Calendar.ExtendedProperties.VALUE, "bar");
1561        extended.put(Calendar.ExtendedProperties.EVENT_ID, eventId);
1562
1563        Uri extendedUri = mResolver.insert(
1564                updatedUri(Calendar.ExtendedProperties.CONTENT_URI, syncAdapter), extended);
1565        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1566        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
1567
1568        // Now test updates
1569
1570        attendee = new ContentValues();
1571        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Sam");
1572        // Need to include EVENT_ID with attendee update.  Is that desired?
1573        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1574
1575        assertEquals("update", 1, mResolver.update(updatedUri(attendeeUri, syncAdapter), attendee,
1576                null /* where */, null /* selectionArgs */));
1577        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1578
1579        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
1580
1581        reminder = new ContentValues();
1582        reminder.put(Calendar.Reminders.MINUTES, 20);
1583
1584        assertEquals("update", 1, mResolver.update(updatedUri(reminderUri, syncAdapter), reminder,
1585                null /* where */, null /* selectionArgs */));
1586        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1587        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
1588
1589        alert = new ContentValues();
1590        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.DISMISSED);
1591
1592        assertEquals("update", 1, mResolver.update(updatedUri(alertUri, syncAdapter), alert,
1593                null /* where */, null /* selectionArgs */));
1594        // Alerts don't dirty the event
1595        testAndClearDirty(eventId, 0);
1596        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
1597
1598        extended = new ContentValues();
1599        extended.put(Calendar.ExtendedProperties.VALUE, "baz");
1600
1601        assertEquals("update", 1, mResolver.update(updatedUri(extendedUri, syncAdapter), extended,
1602                null /* where */, null /* selectionArgs */));
1603        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1604        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
1605
1606        // Now test deletes
1607
1608        assertEquals("delete", 1, mResolver.delete(
1609                updatedUri(attendeeUri, syncAdapter),
1610                null, null /* selectionArgs */));
1611        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1612        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
1613
1614        assertEquals("delete", 1, mResolver.delete(updatedUri(reminderUri, syncAdapter),
1615                null /* where */, null /* selectionArgs */));
1616
1617        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1618        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
1619
1620        assertEquals("delete", 1, mResolver.delete(updatedUri(alertUri, syncAdapter),
1621                null /* where */, null /* selectionArgs */));
1622
1623        // Alerts don't dirty the event
1624        testAndClearDirty(eventId, 0);
1625        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
1626
1627        assertEquals("delete", 1, mResolver.delete(updatedUri(extendedUri, syncAdapter),
1628                null /* where */, null /* selectionArgs */));
1629
1630        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
1631        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 0);
1632    }
1633
1634    /**
1635     * Test calendar deletion
1636     * @throws Exception
1637     */
1638    public void testCalendarDeletion() throws Exception {
1639        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1640        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1641        long eventId = ContentUris.parseId(eventUri);
1642        testAndClearDirty(eventId, 1);
1643        Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1"));
1644        long eventId1 = ContentUris.parseId(eventUri);
1645        assertEquals("delete", 1, mResolver.delete(eventUri1, null, null));
1646        // Calendar has one event and one deleted event
1647        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1648
1649        assertEquals("delete", 1, mResolver.delete(Calendar.Calendars.CONTENT_URI,
1650                "_id=" + mCalendarId, null));
1651        // Calendar should be deleted
1652        testQueryCount(Calendar.Calendars.CONTENT_URI, null, 0);
1653        // Event should be gone
1654        testQueryCount(Calendar.Events.CONTENT_URI, null, 0);
1655    }
1656
1657    /**
1658     * Test multiple account support.
1659     */
1660    public void testMultipleAccounts() throws Exception {
1661        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1662        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1663        Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0"));
1664        Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"));
1665
1666        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1667        Uri eventsWithAccount = Calendar.Events.CONTENT_URI.buildUpon()
1668                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_NAME, "joe@joe.com")
1669                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_TYPE, "com.google")
1670                .build();
1671        // Only one event for that account
1672        testQueryCount(eventsWithAccount, null, 1);
1673
1674        // Test deletion with account and selection
1675
1676        long eventId = ContentUris.parseId(eventUri1);
1677        // Wrong account, should not be deleted
1678        assertEquals("delete", 0, mResolver.delete(
1679                updatedUri(eventsWithAccount, true /* syncAdapter */),
1680                "_id=" + eventId, null /* selectionArgs */));
1681        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
1682        // Right account, should be deleted
1683        assertEquals("delete", 1, mResolver.delete(
1684                updatedUri(Calendar.Events.CONTENT_URI, true /* syncAdapter */),
1685                "_id=" + eventId, null /* selectionArgs */));
1686        testQueryCount(Calendar.Events.CONTENT_URI, null, 1);
1687    }
1688
1689    /**
1690     * Run commands, wiping instance table at each step.
1691     * This tests full instance expansion.
1692     * @throws Exception
1693     */
1694    public void testCommandSequences1() throws Exception {
1695        commandSequences(true);
1696    }
1697
1698    /**
1699     * Run commands normally.
1700     * This tests incremental instance expansion.
1701     * @throws Exception
1702     */
1703    public void testCommandSequences2() throws Exception {
1704        commandSequences(false);
1705    }
1706
1707    /**
1708     * Run thorough set of command sequences
1709     * @param wipe true if instances should be wiped and regenerated
1710     * @throws Exception
1711     */
1712    private void commandSequences(boolean wipe) throws Exception {
1713        Cursor cursor;
1714        Uri url = null;
1715        mWipe = wipe; // Set global flag
1716
1717        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1718
1719        cursor = mResolver.query(mEventsUri, null, null, null, null);
1720        assertEquals(0, cursor.getCount());
1721        cursor.close();
1722        Command[] commands;
1723
1724        Log.i(TAG, "Normal insert/delete");
1725        commands = mNormalInsertDelete;
1726        for (Command command : commands) {
1727            command.execute();
1728        }
1729
1730        deleteAllEvents();
1731
1732        Log.i(TAG, "All-day insert/delete");
1733        commands = mAlldayInsertDelete;
1734        for (Command command : commands) {
1735            command.execute();
1736        }
1737
1738        deleteAllEvents();
1739
1740        Log.i(TAG, "Recurring insert/delete");
1741        commands = mRecurringInsertDelete;
1742        for (Command command : commands) {
1743            command.execute();
1744        }
1745
1746        deleteAllEvents();
1747
1748        Log.i(TAG, "Exception with truncated recurrence");
1749        commands = mExceptionWithTruncatedRecurrence;
1750        for (Command command : commands) {
1751            command.execute();
1752        }
1753
1754        deleteAllEvents();
1755
1756        Log.i(TAG, "Exception with moved recurrence");
1757        commands = mExceptionWithMovedRecurrence;
1758        for (Command command : commands) {
1759            command.execute();
1760        }
1761
1762        deleteAllEvents();
1763
1764        Log.i(TAG, "Exception with cancel");
1765        commands = mCancelInstance;
1766        for (Command command : commands) {
1767            command.execute();
1768        }
1769
1770        deleteAllEvents();
1771
1772        Log.i(TAG, "Exception with moved recurrence2");
1773        commands = mExceptionWithMovedRecurrence2;
1774        for (Command command : commands) {
1775            command.execute();
1776        }
1777
1778        deleteAllEvents();
1779
1780        Log.i(TAG, "Exception with no recurrence");
1781        commands = mExceptionWithNoRecurrence;
1782        for (Command command : commands) {
1783            command.execute();
1784        }
1785    }
1786
1787    /**
1788     * Test Time toString.
1789     * @throws Exception
1790     */
1791    // Suppressed because toString currently hangs.
1792    @Suppress
1793    public void testTimeToString() throws Exception {
1794        Time time = new Time(Time.TIMEZONE_UTC);
1795        String str = "2039-01-01T23:00:00.000Z";
1796        String result = "20390101T230000UTC(0,0,0,-1,0)";
1797        time.parse3339(str);
1798        assertEquals(result, time.toString());
1799    }
1800
1801    /**
1802     * Test the query done by Event.loadEvents
1803     * Also test that instance queries work when an even straddles the expansion range
1804     * @throws Exception
1805     */
1806    public void testInstanceQuery() throws Exception {
1807        final String[] PROJECTION = new String[] {
1808                Instances.TITLE,                 // 0
1809                Instances.EVENT_LOCATION,        // 1
1810                Instances.ALL_DAY,               // 2
1811                Instances.COLOR,                 // 3
1812                Instances.EVENT_TIMEZONE,        // 4
1813                Instances.EVENT_ID,              // 5
1814                Instances.BEGIN,                 // 6
1815                Instances.END,                   // 7
1816                Instances._ID,                   // 8
1817                Instances.START_DAY,             // 9
1818                Instances.END_DAY,               // 10
1819                Instances.START_MINUTE,          // 11
1820                Instances.END_MINUTE,            // 12
1821                Instances.HAS_ALARM,             // 13
1822                Instances.RRULE,                 // 14
1823                Instances.RDATE,                 // 15
1824                Instances.SELF_ATTENDEE_STATUS,  // 16
1825                Events.ORGANIZER,                // 17
1826                Events.GUESTS_CAN_MODIFY,        // 18
1827        };
1828
1829        String orderBy = Instances.SORT_CALENDAR_VIEW;
1830        String where = Instances.SELF_ATTENDEE_STATUS + "!=" + Calendar.Attendees.ATTENDEE_STATUS_DECLINED;
1831
1832        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1833        final String START = "2008-05-01T00:00:00";
1834        final String END = "2008-05-01T20:00:00";
1835
1836        EventInfo[] events = { new EventInfo("normal0",
1837                START,
1838                END,
1839                false /* allDay */,
1840                DEFAULT_TIMEZONE) };
1841
1842        insertEvent(calId, events[0]);
1843
1844        Time time = new Time(DEFAULT_TIMEZONE);
1845        time.parse3339(START);
1846        long startMs = time.toMillis(true /* ignoreDst */);
1847        // Query starting from way in the past to one hour into the event.
1848        // Query is more than 2 months so the range won't get extended by the provider.
1849        Cursor cursor = Instances.query(mResolver, PROJECTION,
1850                startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS,
1851                where, orderBy);
1852        try {
1853            assertEquals(1, cursor.getCount());
1854        } finally {
1855            cursor.close();
1856        }
1857
1858        // Now expand the instance range.  The event overlaps the new part of the range.
1859        cursor = Instances.query(mResolver, PROJECTION,
1860                startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS,
1861                where, orderBy);
1862        try {
1863            assertEquals(1, cursor.getCount());
1864        } finally {
1865            cursor.close();
1866        }
1867    }
1868
1869    private Cursor queryInstances(long begin, long end) {
1870        Uri url = Uri.withAppendedPath(Calendar.Instances.CONTENT_URI, begin + "/" + end);
1871        return mResolver.query(url, null, null, null, null);
1872    }
1873
1874    protected static class MockProvider extends ContentProvider {
1875
1876        private String mAuthority;
1877
1878        private int mNumItems = 0;
1879
1880        public MockProvider(String authority) {
1881            mAuthority = authority;
1882        }
1883
1884        @Override
1885        public boolean onCreate() {
1886            return true;
1887        }
1888
1889        @Override
1890        public Cursor query(Uri uri, String[] projection, String selection,
1891                String[] selectionArgs, String sortOrder) {
1892            return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>());
1893        }
1894
1895        @Override
1896        public String getType(Uri uri) {
1897            throw new UnsupportedOperationException();
1898        }
1899
1900        @Override
1901        public Uri insert(Uri uri, ContentValues values) {
1902            mNumItems++;
1903            return Uri.parse("content://" + mAuthority + "/" + mNumItems);
1904        }
1905
1906        @Override
1907        public int delete(Uri uri, String selection, String[] selectionArgs) {
1908            return 0;
1909        }
1910
1911        @Override
1912        public int update(Uri uri, ContentValues values, String selection,
1913                String[] selectionArgs) {
1914            return 0;
1915        }
1916    }
1917
1918    private void cleanCalendarDataTable(SQLiteOpenHelper helper) {
1919        if (null == helper) {
1920            return;
1921        }
1922        SQLiteDatabase db = helper.getWritableDatabase();
1923        db.execSQL("DELETE FROM CalendarCache;");
1924    }
1925
1926    public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException {
1927        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
1928        cleanCalendarDataTable(helper);
1929        CalendarCache cache = new CalendarCache(helper);
1930
1931        boolean hasException = false;
1932        try {
1933            String value = cache.readData(null);
1934        } catch (CalendarCache.CacheException e) {
1935            hasException = true;
1936        }
1937        assertTrue(hasException);
1938
1939        assertNull(cache.readTimezoneDatabaseVersion());
1940
1941        cache.writeTimezoneDatabaseVersion("1234");
1942        assertEquals("1234", cache.readTimezoneDatabaseVersion());
1943
1944        cache.writeTimezoneDatabaseVersion("5678");
1945        assertEquals("5678", cache.readTimezoneDatabaseVersion());
1946    }
1947
1948    private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) {
1949        Uri uri = Uri.parse("content://" + Calendar.AUTHORITY + "/events");
1950        Log.i(TAG, "Looking for EventId = " + eventId);
1951
1952        Cursor cursor = mResolver.query(uri, null, null, null, null);
1953        assertEquals(1, cursor.getCount());
1954
1955        int colIndexTitle = cursor.getColumnIndex(Calendar.Events.TITLE);
1956        int colIndexDtStart = cursor.getColumnIndex(Calendar.Events.DTSTART);
1957        int colIndexDtEnd = cursor.getColumnIndex(Calendar.Events.DTEND);
1958        int colIndexAllDay = cursor.getColumnIndex(Calendar.Events.ALL_DAY);
1959        if (!cursor.moveToNext()) {
1960            Log.e(TAG,"Could not find inserted event");
1961            assertTrue(false);
1962        }
1963        assertEquals(title, cursor.getString(colIndexTitle));
1964        assertEquals(dtStart, cursor.getLong(colIndexDtStart));
1965        assertEquals(dtEnd, cursor.getLong(colIndexDtEnd));
1966        assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0));
1967        cursor.close();
1968    }
1969
1970    public void testChangeTimezoneDB() throws CalendarCache.CacheException {
1971        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1972
1973        Cursor cursor = mResolver.query(Calendar.Events.CONTENT_URI, null, null, null, null);
1974        assertEquals(0, cursor.getCount());
1975        cursor.close();
1976
1977        EventInfo[] events = { new EventInfo("normal0",
1978                                        "2008-05-01T00:00:00",
1979                                        "2008-05-02T00:00:00",
1980                                        false,
1981                                        DEFAULT_TIMEZONE) };
1982
1983        Uri uri = insertEvent(calId, events[0]);
1984        assertNotNull(uri);
1985
1986        // check the inserted event
1987        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
1988
1989        // inject a new time zone
1990        getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE,
1991                MOCK_TIME_ZONE_DATABASE_VERSION);
1992
1993        // check timezone database version
1994        assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion());
1995
1996        // check if the inserted event as been updated with the timezone information
1997        // there is 1h time difference between America/LosAngeles and America/Anchorage
1998        long deltaMillisForTimezones = 3600000L;
1999        checkEvent(1, events[0].mTitle,
2000                events[0].mDtstart + deltaMillisForTimezones,
2001                events[0].mDtend + deltaMillisForTimezones,
2002                events[0].mAllDay);
2003    }
2004
2005    public static final Uri PROPERTIES_CONTENT_URI =
2006            Uri.parse("content://" + Calendar.AUTHORITY + "/properties");
2007
2008    public static final int COLUMN_KEY_INDEX = 1;
2009    public static final int COLUMN_VALUE_INDEX = 0;
2010
2011    public void testGetProviderProperties() throws CalendarCache.CacheException {
2012        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2013        cleanCalendarDataTable(helper);
2014        CalendarCache cache = new CalendarCache(helper);
2015
2016        cache.writeTimezoneDatabaseVersion("2010k");
2017        cache.writeTimezoneInstances("America/Denver");
2018        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2019        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2020
2021        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, null, null, null);
2022        assertEquals(4, cursor.getCount());
2023
2024        assertEquals(CalendarCache.COLUMN_NAME_KEY, cursor.getColumnName(COLUMN_KEY_INDEX));
2025        assertEquals(CalendarCache.COLUMN_NAME_VALUE, cursor.getColumnName(COLUMN_VALUE_INDEX));
2026
2027        Map<String, String> map = new HashMap<String, String>();
2028
2029        while (cursor.moveToNext()) {
2030            String key = cursor.getString(COLUMN_KEY_INDEX);
2031            String value = cursor.getString(COLUMN_VALUE_INDEX);
2032            map.put(key, value);
2033        }
2034
2035        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2036        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_TYPE));
2037        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES));
2038        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2039
2040        assertEquals("2010k", map.get(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2041        assertEquals("America/Denver", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES));
2042        assertEquals("America/Los_Angeles", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2043        assertEquals(CalendarCache.TIMEZONE_TYPE_AUTO, map.get(CalendarCache.KEY_TIMEZONE_TYPE));
2044
2045        cursor.close();
2046    }
2047
2048    public void testGetProviderPropertiesByKey() throws CalendarCache.CacheException {
2049        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2050        cleanCalendarDataTable(helper);
2051        CalendarCache cache = new CalendarCache(helper);
2052
2053        cache.writeTimezoneDatabaseVersion("2010k");
2054        cache.writeTimezoneInstances("America/Denver");
2055        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2056        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2057
2058        checkValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2059        checkValueForKey("2010k", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2060        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2061        checkValueForKey("America/Los_Angeles", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2062    }
2063
2064    private void checkValueForKey(String value, String key) {
2065        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null,
2066                "key=?", new String[] {key}, null);
2067
2068        assertEquals(1, cursor.getCount());
2069        assertTrue(cursor.moveToFirst());
2070        assertEquals(cursor.getString(COLUMN_KEY_INDEX), key);
2071        assertEquals(cursor.getString(COLUMN_VALUE_INDEX), value);
2072
2073        cursor.close();
2074    }
2075
2076    public void testUpdateProviderProperties() throws CalendarCache.CacheException {
2077        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2078        cleanCalendarDataTable(helper);
2079        CalendarCache cache = new CalendarCache(helper);
2080
2081        String localTimezone = TimeZone.getDefault().getID();
2082
2083        // Set initial value
2084        cache.writeTimezoneDatabaseVersion("2010k");
2085
2086        updateValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2087        checkValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2088
2089        // Set initial values
2090        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2091        cache.writeTimezoneInstances("America/Chicago");
2092        cache.writeTimezoneInstancesPrevious("America/Denver");
2093
2094        updateValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2095        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2096        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2097
2098        updateValueForKey(CalendarCache.TIMEZONE_TYPE_HOME, CalendarCache.KEY_TIMEZONE_TYPE);
2099        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2100        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2101
2102        // Set initial value
2103        cache.writeTimezoneInstancesPrevious("");
2104        updateValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2105        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2106        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2107    }
2108
2109    private void updateValueForKey(String value, String key) {
2110        ContentValues contentValues = new ContentValues();
2111        contentValues.put(CalendarCache.COLUMN_NAME_VALUE, value);
2112
2113        int result = mResolver.update(PROPERTIES_CONTENT_URI,
2114                contentValues,
2115                CalendarCache.COLUMN_NAME_KEY + "=?",
2116                new String[] {key});
2117
2118        assertEquals(1, result);
2119    }
2120}
2121