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