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