CalendarProvider2Test.java revision 4755452ab84f704f8ce4d7e0bf61a9faeeee2b99
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.calendar;
18
19import android.content.ComponentName;
20import android.content.ContentProvider;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.database.MatrixCursor;
29import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.net.Uri;
32import android.provider.CalendarContract;
33import android.provider.CalendarContract.Calendars;
34import android.provider.CalendarContract.Colors;
35import android.provider.CalendarContract.Events;
36import android.provider.CalendarContract.Instances;
37import android.test.AndroidTestCase;
38import android.test.IsolatedContext;
39import android.test.RenamingDelegatingContext;
40import android.test.mock.MockContentResolver;
41import android.test.mock.MockContext;
42import android.test.suitebuilder.annotation.SmallTest;
43import android.test.suitebuilder.annotation.Smoke;
44import android.test.suitebuilder.annotation.Suppress;
45import android.text.TextUtils;
46import android.text.format.DateUtils;
47import android.text.format.Time;
48import android.util.Log;
49
50import java.io.File;
51import java.util.Arrays;
52import java.util.HashMap;
53import java.util.Map;
54import java.util.TimeZone;
55
56/**
57 * Runs various tests on an isolated Calendar provider with its own database.
58 *
59 * You can run the tests with the following command line:
60 *
61 * adb shell am instrument
62 * -e debug false
63 * -w
64 * -e class com.android.providers.calendar.CalendarProvider2Test
65 * com.android.providers.calendar.tests/android.test.InstrumentationTestRunner
66 *
67 * This test no longer extends ProviderTestCase2 because it actually doesn't
68 * allow you to inject a custom context (which we needed to mock out the calls
69 * to start a service). We the next best thing, which is copy the relevant code
70 * from PTC2 and extend AndroidTestCase instead.
71 */
72// flaky test, add back to LargeTest when fixed - bug 2395696
73// @LargeTest
74public class CalendarProvider2Test extends AndroidTestCase {
75    static final String TAG = "calendar";
76
77    private static final String DEFAULT_ACCOUNT_TYPE = "com.google";
78    private static final String DEFAULT_ACCOUNT = "joe@joe.com";
79
80
81    private static final String WHERE_CALENDARS_SELECTED = Calendars.VISIBLE + "=?";
82    private static final String[] WHERE_CALENDARS_ARGS = {
83        "1"
84    };
85    private static final String DEFAULT_SORT_ORDER = "begin ASC";
86
87    private CalendarProvider2ForTesting mProvider;
88    private SQLiteDatabase mDb;
89    private MetaData mMetaData;
90    private Context mContext;
91    private MockContentResolver mResolver;
92    private Uri mEventsUri = Events.CONTENT_URI;
93    private Uri mCalendarsUri = Calendars.CONTENT_URI;
94    private int mCalendarId;
95
96    protected boolean mWipe = false;
97    protected boolean mForceDtend = false;
98
99    // We need a unique id to put in the _sync_id field so that we can create
100    // recurrence exceptions that refer to recurring events.
101    private int mGlobalSyncId = 1000;
102    private static final String CALENDAR_URL =
103            "http://www.google.com/calendar/feeds/joe%40joe.com/private/full";
104
105    private static final String TIME_ZONE_AMERICA_ANCHORAGE = "America/Anchorage";
106    private static final String TIME_ZONE_AMERICA_LOS_ANGELES = "America/Los_Angeles";
107    private static final String DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES;
108
109    private static final String MOCK_TIME_ZONE_DATABASE_VERSION = "2010a";
110
111    private static final long ONE_MINUTE_MILLIS = 60*1000;
112    private static final long ONE_HOUR_MILLIS = 3600*1000;
113    private static final long ONE_WEEK_MILLIS = 7 * 24 * 3600 * 1000;
114
115    /**
116     * We need a few more stub methods so that our tests can run
117     */
118    protected class MockContext2 extends MockContext {
119
120        @Override
121        public String getPackageName() {
122            return getContext().getPackageName();
123        }
124
125        @Override
126        public Resources getResources() {
127            return getContext().getResources();
128        }
129
130        @Override
131        public File getDir(String name, int mode) {
132            // name the directory so the directory will be seperated from
133            // one created through the regular Context
134            return getContext().getDir("mockcontext2_" + name, mode);
135        }
136
137        @Override
138        public ComponentName startService(Intent service) {
139            return null;
140        }
141
142        @Override
143        public boolean stopService(Intent service) {
144            return false;
145        }
146    }
147
148    /**
149     * KeyValue is a simple class that stores a pair of strings representing
150     * a (key, value) pair.  This is used for updating events.
151     */
152    private class KeyValue {
153        String key;
154        String value;
155
156        public KeyValue(String key, String value) {
157            this.key = key;
158            this.value = value;
159        }
160    }
161
162    /**
163     * A generic command interface.  This is used to support a sequence of
164     * commands that can create events, delete or update events, and then
165     * check that the state of the database is as expected.
166     */
167    private interface Command {
168        public void execute();
169    }
170
171    /**
172     * This is used to insert a new event into the database.  The event is
173     * specified by its name (or "title").  All of the event fields (the
174     * start and end time, whether it is an all-day event, and so on) are
175     * stored in a separate table (the "mEvents" table).
176     */
177    private class Insert implements Command {
178        EventInfo eventInfo;
179
180        public Insert(String eventName) {
181            eventInfo = findEvent(eventName);
182        }
183
184        public void execute() {
185            Log.i(TAG, "insert " + eventInfo.mTitle);
186            insertEvent(mCalendarId, eventInfo);
187        }
188    }
189
190    /**
191     * This is used to delete an event, specified by the event name.
192     */
193    private class Delete implements Command {
194        String eventName;
195        String account;
196        String accountType;
197        int expected;
198
199        public Delete(String eventName, int expected, String account, String accountType) {
200            this.eventName = eventName;
201            this.expected = expected;
202            this.account = account;
203            this.accountType = accountType;
204        }
205
206        public void execute() {
207            Log.i(TAG, "delete " + eventName);
208            int rows = deleteMatchingEvents(eventName, account, accountType);
209            assertEquals(expected, rows);
210        }
211    }
212
213    /**
214     * This is used to update an event.  The values to update are specified
215     * with an array of (key, value) pairs.  Both the key and value are
216     * specified as strings.  Event fields that are not really strings (such
217     * as DTSTART which is a long) should be converted to the appropriate type
218     * but that isn't supported yet.  When needed, that can be added here
219     * by checking for specific keys and converting the associated values.
220     */
221    private class Update implements Command {
222        String eventName;
223        KeyValue[] pairs;
224
225        public Update(String eventName, KeyValue[] pairs) {
226            this.eventName = eventName;
227            this.pairs = pairs;
228        }
229
230        public void execute() {
231            Log.i(TAG, "update " + eventName);
232            if (mWipe) {
233                // Wipe instance table so it will be regenerated
234                mMetaData.clearInstanceRange();
235            }
236            ContentValues map = new ContentValues();
237            for (KeyValue pair : pairs) {
238                String value = pair.value;
239                if (CalendarContract.Events.STATUS.equals(pair.key)) {
240                    // Do type conversion for STATUS
241                    map.put(pair.key, Integer.parseInt(value));
242                } else {
243                    map.put(pair.key, value);
244                }
245            }
246            if (map.size() == 1 && map.containsKey(Events.STATUS)) {
247                updateMatchingEventsStatusOnly(eventName, map);
248            } else {
249                updateMatchingEvents(eventName, map);
250            }
251        }
252    }
253
254    /**
255     * This command queries the number of events and compares it to the given
256     * expected value.
257     */
258    private class QueryNumEvents implements Command {
259        int expected;
260
261        public QueryNumEvents(int expected) {
262            this.expected = expected;
263        }
264
265        public void execute() {
266            Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
267            assertEquals(expected, cursor.getCount());
268            cursor.close();
269        }
270    }
271
272
273    /**
274     * This command dumps the list of events to the log for debugging.
275     */
276    private class DumpEvents implements Command {
277
278        public DumpEvents() {
279        }
280
281        public void execute() {
282            Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
283            dumpCursor(cursor);
284            cursor.close();
285        }
286    }
287
288    /**
289     * This command dumps the list of instances to the log for debugging.
290     */
291    private class DumpInstances implements Command {
292        long begin;
293        long end;
294
295        public DumpInstances(String startDate, String endDate) {
296            Time time = new Time(DEFAULT_TIMEZONE);
297            time.parse3339(startDate);
298            begin = time.toMillis(false /* use isDst */);
299            time.parse3339(endDate);
300            end = time.toMillis(false /* use isDst */);
301        }
302
303        public void execute() {
304            Cursor cursor = queryInstances(begin, end);
305            dumpCursor(cursor);
306            cursor.close();
307        }
308    }
309
310    /**
311     * This command queries the number of instances and compares it to the given
312     * expected value.
313     */
314    private class QueryNumInstances implements Command {
315        int expected;
316        long begin;
317        long end;
318
319        public QueryNumInstances(String startDate, String endDate, int expected) {
320            Time time = new Time(DEFAULT_TIMEZONE);
321            time.parse3339(startDate);
322            begin = time.toMillis(false /* use isDst */);
323            time.parse3339(endDate);
324            end = time.toMillis(false /* use isDst */);
325            this.expected = expected;
326        }
327
328        public void execute() {
329            Cursor cursor = queryInstances(begin, end);
330            assertEquals(expected, cursor.getCount());
331            cursor.close();
332        }
333    }
334
335    /**
336     * When this command runs it verifies that all of the instances in the
337     * given range match the expected instances (each instance is specified by
338     * a start date).
339     * If you just want to verify that an instance exists in a given date
340     * range, use {@link VerifyInstance} instead.
341     */
342    private class VerifyAllInstances implements Command {
343        long[] instances;
344        long begin;
345        long end;
346
347        public VerifyAllInstances(String startDate, String endDate, String[] dates) {
348            Time time = new Time(DEFAULT_TIMEZONE);
349            time.parse3339(startDate);
350            begin = time.toMillis(false /* use isDst */);
351            time.parse3339(endDate);
352            end = time.toMillis(false /* use isDst */);
353
354            if (dates == null) {
355                return;
356            }
357
358            // Convert all the instance date strings to UTC milliseconds
359            int len = dates.length;
360            this.instances = new long[len];
361            int index = 0;
362            for (String instance : dates) {
363                time.parse3339(instance);
364                this.instances[index++] = time.toMillis(false /* use isDst */);
365            }
366        }
367
368        public void execute() {
369            Cursor cursor = queryInstances(begin, end);
370            int len = 0;
371            if (instances != null) {
372                len = instances.length;
373            }
374            if (len != cursor.getCount()) {
375                dumpCursor(cursor);
376            }
377            assertEquals("number of instances don't match", len, cursor.getCount());
378
379            if (instances == null) {
380                return;
381            }
382
383            int beginColumn = cursor.getColumnIndex(Instances.BEGIN);
384            while (cursor.moveToNext()) {
385                long begin = cursor.getLong(beginColumn);
386
387                // Search the list of expected instances for a matching start
388                // time.
389                boolean found = false;
390                for (long instance : instances) {
391                    if (instance == begin) {
392                        found = true;
393                        break;
394                    }
395                }
396                if (!found) {
397                    int titleColumn = cursor.getColumnIndex(Events.TITLE);
398                    int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY);
399
400                    String title = cursor.getString(titleColumn);
401                    boolean allDay = cursor.getInt(allDayColumn) != 0;
402                    int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE |
403                            DateUtils.FORMAT_24HOUR;
404                    if (allDay) {
405                        flags |= DateUtils.FORMAT_UTC;
406                    } else {
407                        flags |= DateUtils.FORMAT_SHOW_TIME;
408                    }
409                    String date = DateUtils.formatDateRange(mContext, begin, begin, flags);
410                    String mesg = String.format("Test failed!"
411                            + " unexpected instance (\"%s\") at %s",
412                            title, date);
413                    Log.e(TAG, mesg);
414                }
415                if (!found) {
416                    dumpCursor(cursor);
417                }
418                assertTrue(found);
419            }
420            cursor.close();
421        }
422    }
423
424    /**
425     * When this command runs it verifies that the given instance exists in
426     * the given date range.
427     */
428    private class VerifyInstance implements Command {
429        long instance;
430        boolean allDay;
431        long begin;
432        long end;
433
434        /**
435         * Creates a command to check that the given range [startDate,endDate]
436         * contains a specific instance of an event (specified by "date").
437         *
438         * @param startDate the beginning of the date range
439         * @param endDate the end of the date range
440         * @param date the date or date-time string of an event instance
441         */
442        public VerifyInstance(String startDate, String endDate, String date) {
443            Time time = new Time(DEFAULT_TIMEZONE);
444            time.parse3339(startDate);
445            begin = time.toMillis(false /* use isDst */);
446            time.parse3339(endDate);
447            end = time.toMillis(false /* use isDst */);
448
449            // Convert the instance date string to UTC milliseconds
450            time.parse3339(date);
451            allDay = time.allDay;
452            instance = time.toMillis(false /* use isDst */);
453        }
454
455        public void execute() {
456            Cursor cursor = queryInstances(begin, end);
457            int beginColumn = cursor.getColumnIndex(Instances.BEGIN);
458            boolean found = false;
459            while (cursor.moveToNext()) {
460                long begin = cursor.getLong(beginColumn);
461
462                if (instance == begin) {
463                    found = true;
464                    break;
465                }
466            }
467            if (!found) {
468                int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE;
469                if (allDay) {
470                    flags |= DateUtils.FORMAT_UTC;
471                } else {
472                    flags |= DateUtils.FORMAT_SHOW_TIME;
473                }
474                String date = DateUtils.formatDateRange(mContext, instance, instance, flags);
475                String mesg = String.format("Test failed!"
476                        + " cannot find instance at %s",
477                        date);
478                Log.e(TAG, mesg);
479            }
480            assertTrue(found);
481            cursor.close();
482        }
483    }
484
485    /**
486     * This class stores all the useful information about an event.
487     */
488    private class EventInfo {
489        String mTitle;
490        String mDescription;
491        String mTimezone;
492        boolean mAllDay;
493        long mDtstart;
494        long mDtend;
495        String mRrule;
496        String mDuration;
497        String mOriginalTitle;
498        long mOriginalInstance;
499        int mSyncId;
500
501        // Constructor for normal events, using the default timezone
502        public EventInfo(String title, String startDate, String endDate,
503                boolean allDay) {
504            init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE);
505        }
506
507        // Constructor for normal events, specifying the timezone
508        public EventInfo(String title, String startDate, String endDate,
509                boolean allDay, String timezone) {
510            init(title, startDate, endDate, allDay, timezone);
511        }
512
513        public void init(String title, String startDate, String endDate,
514                boolean allDay, String timezone) {
515            mTitle = title;
516            Time time = new Time();
517            if (allDay) {
518                time.timezone = Time.TIMEZONE_UTC;
519            } else if (timezone != null) {
520                time.timezone = timezone;
521            }
522            mTimezone = time.timezone;
523            time.parse3339(startDate);
524            mDtstart = time.toMillis(false /* use isDst */);
525            time.parse3339(endDate);
526            mDtend = time.toMillis(false /* use isDst */);
527            mDuration = null;
528            mRrule = null;
529            mAllDay = allDay;
530        }
531
532        // Constructor for repeating events, using the default timezone
533        public EventInfo(String title, String description, String startDate, String endDate,
534                String rrule, boolean allDay) {
535            init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE);
536        }
537
538        // Constructor for repeating events, specifying the timezone
539        public EventInfo(String title, String description, String startDate, String endDate,
540                String rrule, boolean allDay, String timezone) {
541            init(title, description, startDate, endDate, rrule, allDay, timezone);
542        }
543
544        public void init(String title, String description, String startDate, String endDate,
545                String rrule, boolean allDay, String timezone) {
546            mTitle = title;
547            mDescription = description;
548            Time time = new Time();
549            if (allDay) {
550                time.timezone = Time.TIMEZONE_UTC;
551            } else if (timezone != null) {
552                time.timezone = timezone;
553            }
554            mTimezone = time.timezone;
555            time.parse3339(startDate);
556            mDtstart = time.toMillis(false /* use isDst */);
557            if (endDate != null) {
558                time.parse3339(endDate);
559                mDtend = time.toMillis(false /* use isDst */);
560            }
561            if (allDay) {
562                long days = 1;
563                if (endDate != null) {
564                    days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS;
565                }
566                mDuration = "P" + days + "D";
567            } else {
568                long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS;
569                mDuration = "P" + seconds + "S";
570            }
571            mRrule = rrule;
572            mAllDay = allDay;
573        }
574
575        // Constructor for recurrence exceptions, using the default timezone
576        public EventInfo(String originalTitle, String originalInstance, String title,
577                String description, String startDate, String endDate, boolean allDay) {
578            init(originalTitle, originalInstance,
579                    title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE);
580        }
581
582        public void init(String originalTitle, String originalInstance,
583                String title, String description, String startDate, String endDate,
584                boolean allDay, String timezone) {
585            mOriginalTitle = originalTitle;
586            Time time = new Time(timezone);
587            time.parse3339(originalInstance);
588            mOriginalInstance = time.toMillis(false /* use isDst */);
589            init(title, description, startDate, endDate, null /* rrule */, allDay, timezone);
590        }
591    }
592
593    private class InstanceInfo {
594        EventInfo mEvent;
595        long mBegin;
596        long mEnd;
597        int mExpectedOccurrences;
598
599        public InstanceInfo(String eventName, String startDate, String endDate, int expected) {
600            // Find the test index that contains the given event name
601            mEvent = findEvent(eventName);
602            Time time = new Time(mEvent.mTimezone);
603            time.parse3339(startDate);
604            mBegin = time.toMillis(false /* use isDst */);
605            time.parse3339(endDate);
606            mEnd = time.toMillis(false /* use isDst */);
607            mExpectedOccurrences = expected;
608        }
609    }
610
611    /**
612     * This is the main table of events.  The events in this table are
613     * referred to by name in other places.
614     */
615    private EventInfo[] mEvents = {
616            new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false),
617            new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false),
618            new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false),
619            new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true),
620            new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true),
621            new EventInfo("daily0", "daily from 5/1/2008 12am to 1am",
622                    "2008-05-01T00:00:00", "2008-05-01T01:00:00",
623                    "FREQ=DAILY;WKST=SU", false),
624            new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am",
625                    "2008-05-01T08:30:00", "2008-05-01T09:30:00",
626                    "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false),
627            new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am",
628                    "2008-05-01T08:45:00", "2008-05-01T09:15:00",
629                    "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false),
630            new EventInfo("allday daily0", "all-day daily from 5/1/2008",
631                    "2008-05-01", null,
632                    "FREQ=DAILY;WKST=SU", true),
633            new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008",
634                    "2008-05-01", null,
635                    "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true),
636            new EventInfo("allday weekly0", "all-day weekly from 5/1/2008",
637                    "2008-05-01", null,
638                    "FREQ=WEEKLY;WKST=SU", true),
639            new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008",
640                    "2008-05-01", "2008-05-03",
641                    "FREQ=WEEKLY;WKST=SU", true),
642            new EventInfo("allday yearly0", "all-day yearly on 5/1/2008",
643                    "2008-05-01T", null,
644                    "FREQ=YEARLY;WKST=SU", true),
645            new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm",
646                    "2008-05-06T13:00:00", "2008-05-06T14:00:00",
647                    "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false),
648            new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm",
649                    "2008-05-06T14:30:00", "2008-05-06T15:30:00",
650                    "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false),
651            new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm",
652                    "2008-05-20T15:00:00", "2008-05-20T16:00:00",
653                    "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false),
654            new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am",
655                    "2008-05-01T00:00:00", "2008-05-01T00:10:00",
656                    "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false),
657            new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight",
658                    "2008-05-31T23:00:00", "2008-06-01T00:00:00",
659                    "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false),
660            new EventInfo("daily0", "2008-05-01T00:00:00",
661                    "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am",
662                    "2008-05-01T02:00:00", "2008-05-01T01:03:00", false),
663            new EventInfo("daily0", "2008-05-03T00:00:00",
664                    "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am",
665                    "2008-05-03T02:00:00", "2008-05-03T01:03:00", false),
666            new EventInfo("daily0", "2008-05-02T00:00:00",
667                    "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008",
668                    "2008-01-02T00:00:00", "2008-01-02T01:00:00", false),
669            new EventInfo("weekly0", "2008-05-13T13:00:00",
670                    "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm",
671                    "2008-12-11T13:00:00", "2008-12-11T14:00:00", false),
672            new EventInfo("weekly0", "2008-05-13T13:00:00",
673                    "cancel0", "weekly0 exception for 5/13/2008 1pm",
674                    "2008-05-13T13:00:00", "2008-05-13T14:00:00", false),
675            new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm",
676                    "2008-05-01T13:00:00", "2008-05-01T14:00:00",
677                    "FREQ=YEARLY;WKST=SU", false),
678    };
679
680    /**
681     * This table is used to verify the events generated by mEvents.  It checks that the
682     * number of instances within a given range matches the expected number
683     * of instances.
684     */
685    private InstanceInfo[] mInstanceRanges = {
686            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1),
687            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
688            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2),
689            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2),
690            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
691            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1),
692            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2),
693            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31),
694            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32),
695
696            new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
697            new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2),
698
699            new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
700            new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3),
701
702            new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7),
703            new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3),
704            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1),
705            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2),
706            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5),
707            new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5),
708            new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1),
709            new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2),
710
711            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
712            new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
713            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4),
714            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8),
715
716            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
717            new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
718            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2),
719            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4),
720
721            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0),
722            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1),
723            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0),
724            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0),
725            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1),
726            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
727            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2),
728
729            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
730            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
731            new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1),
732            new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0),
733            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2),
734
735            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0),
736            new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1),
737            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1),
738            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2),
739
740            new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1),
741            new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2),
742    };
743
744    /**
745     * This sequence of commands inserts and deletes some events.
746     */
747    private Command[] mNormalInsertDelete = {
748            new Insert("normal0"),
749            new Insert("normal1"),
750            new Insert("normal2"),
751            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3),
752            new Delete("normal1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
753            new QueryNumEvents(2),
754            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2),
755            new Delete("normal1", 0, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
756            new Delete("normal2", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
757            new QueryNumEvents(1),
758            new Delete("normal0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
759            new QueryNumEvents(0),
760    };
761
762    /**
763     * This sequence of commands inserts and deletes some all-day events.
764     */
765    private Command[] mAlldayInsertDelete = {
766            new Insert("allday0"),
767            new Insert("allday1"),
768            new QueryNumEvents(2),
769            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0),
770            new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2),
771            new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
772            new Delete("allday0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
773            new QueryNumEvents(1),
774            new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
775            new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
776            new Delete("allday1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
777            new QueryNumEvents(0),
778    };
779
780    /**
781     * This sequence of commands inserts and deletes some repeating events.
782     */
783    private Command[] mRecurringInsertDelete = {
784            new Insert("daily0"),
785            new Insert("daily1"),
786            new QueryNumEvents(2),
787            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3),
788            new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2),
789            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6),
790            new Delete("daily1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
791            new QueryNumEvents(1),
792            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2),
793            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4),
794            new Delete("daily0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
795            new QueryNumEvents(0),
796    };
797
798    /**
799     * This sequence of commands creates a recurring event with a recurrence
800     * exception that moves an event outside the expansion window.  It checks that the
801     * recurrence exception does not occur in the Instances database table.
802     * Bug 1642665
803     */
804    private Command[] mExceptionWithMovedRecurrence = {
805            new Insert("daily0"),
806            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
807                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
808                            "2008-05-03T00:00:00", }),
809            new Insert("except2"),
810            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
811                    new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}),
812    };
813
814    /**
815     * This sequence of commands deletes (cancels) one instance of a recurrence.
816     */
817    private Command[] mCancelInstance = {
818            new Insert("weekly0"),
819            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
820                    new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00",
821                            "2008-05-20T13:00:00", }),
822            new Insert("cancel0"),
823            new Update("cancel0", new KeyValue[] {
824                    new KeyValue(CalendarContract.Events.STATUS,
825                        Integer.toString(CalendarContract.Events.STATUS_CANCELED)),
826            }),
827            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
828                    new String[] {"2008-05-06T13:00:00",
829                            "2008-05-20T13:00:00", }),
830    };
831    /**
832     * This sequence of commands creates a recurring event with a recurrence
833     * exception that moves an event from outside the expansion window into the
834     * expansion window.
835     */
836    private Command[] mExceptionWithMovedRecurrence2 = {
837            new Insert("weekly0"),
838            new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
839                    new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
840                            "2008-12-16T13:00:00", }),
841            new Insert("except3"),
842            new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
843                    new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
844                            "2008-12-11T13:00:00", "2008-12-16T13:00:00", }),
845    };
846    /**
847     * This sequence of commands creates a recurring event with a recurrence
848     * exception and then changes the end time of the recurring event.  It then
849     * checks that the recurrence exception does not occur in the Instances
850     * database table.
851     */
852    private Command[]
853            mExceptionWithTruncatedRecurrence = {
854            new Insert("daily0"),
855            // Verify 4 occurrences of the "daily0" repeating event
856            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
857                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
858                            "2008-05-03T00:00:00", "2008-05-04T00:00:00"}),
859            new Insert("except1"),
860            new QueryNumEvents(2),
861
862            // Verify that one of the 4 occurrences has its start time changed
863            // so that it now matches the recurrence exception.
864            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
865                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
866                            "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
867
868            // Change the end time of "daily0" but it still includes the
869            // recurrence exception.
870            new Update("daily0", new KeyValue[] {
871                    new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"),
872            }),
873
874            // Verify that the recurrence exception is still there
875            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
876                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
877                            "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
878            // This time change the end time of "daily0" so that it excludes
879            // the recurrence exception.
880            new Update("daily0", new KeyValue[] {
881                    new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"),
882            }),
883            // The server will cancel the out-of-range exception.
884            // It would be nice for the provider to handle this automatically,
885            // but for now simulate the server-side cancel.
886            new Update("except1", new KeyValue[] {
887                new KeyValue(CalendarContract.Events.STATUS,
888                        Integer.toString(CalendarContract.Events.STATUS_CANCELED)),
889            }),
890            // Verify that the recurrence exception does not appear.
891            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
892                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}),
893    };
894
895    /**
896     * Bug 135848.  Ensure that a recurrence exception is displayed even if the recurrence
897     * is not present.
898     */
899    private Command[] mExceptionWithNoRecurrence = {
900            new Insert("except0"),
901            new QueryNumEvents(1),
902            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
903                    new String[] {"2008-05-01T02:00:00"}),
904    };
905
906    private EventInfo findEvent(String name) {
907        int len = mEvents.length;
908        for (int ii = 0; ii < len; ii++) {
909            EventInfo event = mEvents[ii];
910            if (name.equals(event.mTitle)) {
911                return event;
912            }
913        }
914        return null;
915    }
916
917    @Override
918    protected void setUp() throws Exception {
919        super.setUp();
920        // This code here is the code that was originally in ProviderTestCase2
921        mResolver = new MockContentResolver();
922
923        final String filenamePrefix = "test.";
924        RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(
925                new MockContext2(), // The context that most methods are delegated to
926                getContext(), // The context that file methods are delegated to
927                filenamePrefix);
928        mContext = new IsolatedContext(mResolver, targetContextWrapper);
929
930        mProvider = new CalendarProvider2ForTesting();
931        mProvider.attachInfo(mContext, null);
932
933        mResolver.addProvider(CalendarContract.AUTHORITY, mProvider);
934        mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
935        mResolver.addProvider("sync", new MockProvider("sync"));
936
937        mMetaData = getProvider().mMetaData;
938        mForceDtend = false;
939
940        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
941        mDb = helper.getWritableDatabase();
942        wipeAndInitData(helper, mDb);
943    }
944
945    @Override
946    protected void tearDown() throws Exception {
947        try {
948            mDb.close();
949            mDb = null;
950            getProvider().getDatabaseHelper().close();
951        } catch (IllegalStateException e) {
952            e.printStackTrace();
953        }
954        super.tearDown();
955    }
956
957    public void wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db)
958            throws CalendarCache.CacheException {
959        db.beginTransaction();
960
961        // Clean tables
962        db.delete("Calendars", null, null);
963        db.delete("Events", null, null);
964        db.delete("EventsRawTimes", null, null);
965        db.delete("Instances", null, null);
966        db.delete("CalendarMetaData", null, null);
967        db.delete("CalendarCache", null, null);
968        db.delete("Attendees", null, null);
969        db.delete("Reminders", null, null);
970        db.delete("CalendarAlerts", null, null);
971        db.delete("ExtendedProperties", null, null);
972
973        // Set CalendarCache data
974        initCalendarCacheLocked(helper, db);
975
976        // set CalendarMetaData data
977        long now = System.currentTimeMillis();
978        ContentValues values = new ContentValues();
979        values.put("localTimezone", "America/Los_Angeles");
980        values.put("minInstance", 1207008000000L); // 1st April 2008
981        values.put("maxInstance", now + ONE_WEEK_MILLIS);
982        db.insert("CalendarMetaData", null, values);
983
984        db.setTransactionSuccessful();
985        db.endTransaction();
986    }
987
988    private void initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db)
989            throws CalendarCache.CacheException {
990        CalendarCache cache = new CalendarCache(helper);
991
992        String localTimezone = TimeZone.getDefault().getID();
993
994        // Set initial values
995        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_DATABASE_VERSION, "2010k");
996        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_TYPE, CalendarCache.TIMEZONE_TYPE_AUTO);
997        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES, localTimezone);
998        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS, localTimezone);
999    }
1000
1001    protected CalendarProvider2ForTesting getProvider() {
1002        return mProvider;
1003    }
1004
1005    /**
1006     * Dumps the contents of the given cursor to the log.  For debugging.
1007     * @param cursor the database cursor
1008     */
1009    private void dumpCursor(Cursor cursor) {
1010        cursor.moveToPosition(-1);
1011        String[] cols = cursor.getColumnNames();
1012
1013        Log.i(TAG, "dumpCursor() count: " + cursor.getCount());
1014        int index = 0;
1015        while (cursor.moveToNext()) {
1016            Log.i(TAG, index + " {");
1017            for (int i = 0; i < cols.length; i++) {
1018                Log.i(TAG, "    " + cols[i] + '=' + cursor.getString(i));
1019            }
1020            Log.i(TAG, "}");
1021            index += 1;
1022        }
1023        cursor.moveToPosition(-1);
1024    }
1025
1026    private int insertCal(String name, String timezone) {
1027        return insertCal(name, timezone, DEFAULT_ACCOUNT);
1028    }
1029
1030    /**
1031     * Creates a new calendar, with the provided name, time zone, and account name.
1032     *
1033     * @return the new calendar's _ID value
1034     */
1035    private int insertCal(String name, String timezone, String account) {
1036        ContentValues m = new ContentValues();
1037        m.put(Calendars.NAME, name);
1038        m.put(Calendars.CALENDAR_DISPLAY_NAME, name);
1039        m.put(Calendars.CALENDAR_COLOR, 0xff123456);
1040        m.put(Calendars.CALENDAR_TIME_ZONE, timezone);
1041        m.put(Calendars.VISIBLE, 1);
1042        m.put(Calendars.CAL_SYNC1, CALENDAR_URL);
1043        m.put(Calendars.OWNER_ACCOUNT, account);
1044        m.put(Calendars.ACCOUNT_NAME,  account);
1045        m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1046        m.put(Calendars.SYNC_EVENTS,  1);
1047
1048        Uri url = mResolver.insert(
1049                addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m);
1050        String id = url.getLastPathSegment();
1051        return Integer.parseInt(id);
1052    }
1053
1054    private String obsToString(Object... objs) {
1055        StringBuilder bob = new StringBuilder();
1056
1057        for (Object obj : objs) {
1058            bob.append(obj.toString());
1059            bob.append('#');
1060        }
1061
1062        return bob.toString();
1063    }
1064
1065    private long insertColor(long colorType, String colorKey, long color) {
1066        ContentValues m = new ContentValues();
1067        m.put(Colors.ACCOUNT_NAME, DEFAULT_ACCOUNT);
1068        m.put(Colors.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1069        m.put(Colors.DATA, obsToString(colorType, colorKey, color));
1070        m.put(Colors.COLOR_TYPE, colorType);
1071        m.put(Colors.COLOR_KEY, colorKey);
1072        m.put(Colors.COLOR, color);
1073
1074        Uri uri = CalendarContract.Colors.CONTENT_URI;
1075
1076        uri = mResolver.insert(addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), m);
1077        String id = uri.getLastPathSegment();
1078        return Long.parseLong(id);
1079    }
1080
1081    /**
1082     * Constructs a URI from a base URI (e.g. "content://com.android.calendar/calendars"),
1083     * an account name, and an account type.
1084     */
1085    private Uri addSyncQueryParams(Uri uri, String account, String accountType) {
1086        return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
1087                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
1088                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
1089    }
1090
1091    private int deleteMatchingCalendars(String selection, String[] selectionArgs) {
1092        return mResolver.delete(mCalendarsUri, selection, selectionArgs);
1093    }
1094
1095    private Uri insertEvent(int calId, EventInfo event) {
1096        if (mWipe) {
1097            // Wipe instance table so it will be regenerated
1098            mMetaData.clearInstanceRange();
1099        }
1100        ContentValues m = new ContentValues();
1101        m.put(Events.CALENDAR_ID, calId);
1102        m.put(Events.TITLE, event.mTitle);
1103        m.put(Events.DTSTART, event.mDtstart);
1104        m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
1105
1106        if (event.mRrule == null || mForceDtend) {
1107            // This is a normal event
1108            m.put(Events.DTEND, event.mDtend);
1109            m.remove(Events.DURATION);
1110        }
1111        if (event.mRrule != null) {
1112            // This is a repeating event
1113            m.put(Events.RRULE, event.mRrule);
1114            m.put(Events.DURATION, event.mDuration);
1115            m.remove(Events.DTEND);
1116        }
1117
1118        if (event.mDescription != null) {
1119            m.put(Events.DESCRIPTION, event.mDescription);
1120        }
1121        if (event.mTimezone != null) {
1122            m.put(Events.EVENT_TIMEZONE, event.mTimezone);
1123        }
1124
1125        if (event.mOriginalTitle != null) {
1126            // This is a recurrence exception.
1127            EventInfo recur = findEvent(event.mOriginalTitle);
1128            assertNotNull(recur);
1129            String syncId = String.format("%d", recur.mSyncId);
1130            m.put(Events.ORIGINAL_SYNC_ID, syncId);
1131            m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0);
1132            m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
1133        }
1134        Uri url = mResolver.insert(mEventsUri, m);
1135
1136        // Create a fake _sync_id and add it to the event.  Update the database
1137        // directly so that we don't trigger any validation checks in the
1138        // CalendarProvider.
1139        long id = ContentUris.parseId(url);
1140        mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id);
1141        event.mSyncId = mGlobalSyncId;
1142        mGlobalSyncId += 1;
1143
1144        return url;
1145    }
1146
1147    /**
1148     * Deletes all the events that match the given title.
1149     * @param title the given title to match events on
1150     * @return the number of rows deleted
1151     */
1152    private int deleteMatchingEvents(String title, String account, String accountType) {
1153        Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID },
1154                "title=?", new String[] { title }, null);
1155        int numRows = 0;
1156        while (cursor.moveToNext()) {
1157            long id = cursor.getLong(0);
1158            // Do delete as a sync adapter so event is really deleted, not just marked
1159            // as deleted.
1160            Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true, account,
1161                    accountType);
1162            numRows += mResolver.delete(uri, null, null);
1163        }
1164        cursor.close();
1165        return numRows;
1166    }
1167
1168    /**
1169     * Updates all the events that match the given title.
1170     * @param title the given title to match events on
1171     * @return the number of rows updated
1172     */
1173    private int updateMatchingEvents(String title, ContentValues values) {
1174        String[] projection = new String[] {
1175                Events._ID,
1176                Events.DTSTART,
1177                Events.DTEND,
1178                Events.DURATION,
1179                Events.ALL_DAY,
1180                Events.RRULE,
1181                Events.EVENT_TIMEZONE,
1182                Events.ORIGINAL_SYNC_ID,
1183        };
1184        Cursor cursor = mResolver.query(mEventsUri, projection,
1185                "title=?", new String[] { title }, null);
1186        int numRows = 0;
1187        while (cursor.moveToNext()) {
1188            long id = cursor.getLong(0);
1189
1190            // If any of the following fields are being changed, then we need
1191            // to include all of them.
1192            if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
1193                    || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
1194                    || values.containsKey(Events.RRULE)
1195                    || values.containsKey(Events.EVENT_TIMEZONE)
1196                    || values.containsKey(CalendarContract.Events.STATUS)) {
1197                long dtstart = cursor.getLong(1);
1198                long dtend = cursor.getLong(2);
1199                String duration = cursor.getString(3);
1200                boolean allDay = cursor.getInt(4) != 0;
1201                String rrule = cursor.getString(5);
1202                String timezone = cursor.getString(6);
1203                String originalEvent = cursor.getString(7);
1204
1205                if (!values.containsKey(Events.DTSTART)) {
1206                    values.put(Events.DTSTART, dtstart);
1207                }
1208                // Don't add DTEND for repeating events
1209                if (!values.containsKey(Events.DTEND) && rrule == null) {
1210                    values.put(Events.DTEND, dtend);
1211                }
1212                if (!values.containsKey(Events.DURATION) && duration != null) {
1213                    values.put(Events.DURATION, duration);
1214                }
1215                if (!values.containsKey(Events.ALL_DAY)) {
1216                    values.put(Events.ALL_DAY, allDay ? 1 : 0);
1217                }
1218                if (!values.containsKey(Events.RRULE) && rrule != null) {
1219                    values.put(Events.RRULE, rrule);
1220                }
1221                if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
1222                    values.put(Events.EVENT_TIMEZONE, timezone);
1223                }
1224                if (!values.containsKey(Events.ORIGINAL_SYNC_ID) && originalEvent != null) {
1225                    values.put(Events.ORIGINAL_SYNC_ID, originalEvent);
1226                }
1227            }
1228
1229            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1230            numRows += mResolver.update(uri, values, null, null);
1231        }
1232        cursor.close();
1233        return numRows;
1234    }
1235
1236    /**
1237     * Updates the status of all the events that match the given title.
1238     * @param title the given title to match events on
1239     * @return the number of rows updated
1240     */
1241    private int updateMatchingEventsStatusOnly(String title, ContentValues values) {
1242        String[] projection = new String[] {
1243                Events._ID,
1244        };
1245        if (values.size() != 1 && !values.containsKey(Events.STATUS)) {
1246            return 0;
1247        }
1248        Cursor cursor = mResolver.query(mEventsUri, projection,
1249                "title=?", new String[] { title }, null);
1250        int numRows = 0;
1251        while (cursor.moveToNext()) {
1252            long id = cursor.getLong(0);
1253
1254            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1255            numRows += mResolver.update(uri, values, null, null);
1256        }
1257        cursor.close();
1258        return numRows;
1259    }
1260
1261
1262    private void deleteAllEvents() {
1263        mDb.execSQL("DELETE FROM Events;");
1264        mMetaData.clearInstanceRange();
1265    }
1266
1267    public void testInsertColor() throws Exception {
1268        checkColor(Colors.TYPE_EVENT, "1" /* color key */, 11 /* color */);
1269        try {
1270            checkColor(Colors.TYPE_EVENT, "1" /* color key */, 11 /* color */);
1271            fail("Expected to fail with duplicate insertion");
1272        } catch (IllegalArgumentException iae) {
1273            // good
1274        }
1275
1276        checkColor(Colors.TYPE_CALENDAR, "1" /* color key */, 22 /* color */);
1277    }
1278
1279    private void checkColor(long colorType, String colorKey, long color) {
1280        String[] projection = new String[] {
1281                Colors.ACCOUNT_NAME, // 0
1282                Colors.ACCOUNT_TYPE, // 1
1283                Colors.COLOR_TYPE,   // 2
1284                Colors.COLOR_KEY,    // 3
1285                Colors.COLOR,        // 4
1286                Colors._ID,          // 5
1287                Colors.DATA,         // 6
1288        };
1289
1290        long color1Id = insertColor(colorType, colorKey, color);
1291
1292        Cursor cursor = mResolver.query(Colors.CONTENT_URI, projection, Colors.COLOR_KEY
1293                + "=? AND " + Colors.COLOR_TYPE + "=?", new String[] {
1294                colorKey, Long.toString(colorType)
1295        }, null /* sortOrder */);
1296
1297        assertEquals(1, cursor.getCount());
1298
1299        assertTrue(cursor.moveToFirst());
1300        assertEquals(DEFAULT_ACCOUNT, cursor.getString(0));
1301        assertEquals(DEFAULT_ACCOUNT_TYPE, cursor.getString(1));
1302        assertEquals(colorType, cursor.getLong(2));
1303        assertEquals(colorKey, cursor.getString(3));
1304        assertEquals(color, cursor.getLong(4));
1305        assertEquals(color1Id, cursor.getLong(5));
1306        assertEquals(obsToString(colorType, colorKey, color), cursor.getString(6));
1307        cursor.close();
1308    }
1309
1310    public void testInsertNormalEvents() throws Exception {
1311        Cursor cursor;
1312        Uri url = null;
1313
1314        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1315
1316        cursor = mResolver.query(mEventsUri, null, null, null, null);
1317        assertEquals(0, cursor.getCount());
1318        cursor.close();
1319
1320        // Keep track of the number of normal events
1321        int numEvents = 0;
1322
1323        // "begin" is the earliest start time of all the normal events,
1324        // and "end" is the latest end time of all the normal events.
1325        long begin = 0, end = 0;
1326
1327        int len = mEvents.length;
1328        for (int ii = 0; ii < len; ii++) {
1329            EventInfo event = mEvents[ii];
1330            // Skip repeating events and recurrence exceptions
1331            if (event.mRrule != null || event.mOriginalTitle != null) {
1332                continue;
1333            }
1334            if (numEvents == 0) {
1335                begin = event.mDtstart;
1336                end = event.mDtend;
1337            } else {
1338                if (begin > event.mDtstart) {
1339                    begin = event.mDtstart;
1340                }
1341                if (end < event.mDtend) {
1342                    end = event.mDtend;
1343                }
1344            }
1345            url = insertEvent(calId, event);
1346            numEvents += 1;
1347        }
1348
1349        // query one
1350        cursor = mResolver.query(url, null, null, null, null);
1351        assertEquals(1, cursor.getCount());
1352        cursor.close();
1353
1354        // query all
1355        cursor = mResolver.query(mEventsUri, null, null, null, null);
1356        assertEquals(numEvents, cursor.getCount());
1357        cursor.close();
1358
1359        // Check that the Instances table has one instance of each of the
1360        // normal events.
1361        cursor = queryInstances(begin, end);
1362        assertEquals(numEvents, cursor.getCount());
1363        cursor.close();
1364    }
1365
1366    public void testInsertRepeatingEvents() throws Exception {
1367        Cursor cursor;
1368        Uri url = null;
1369
1370        int calId = insertCal("Calendar0", "America/Los_Angeles");
1371
1372        cursor = mResolver.query(mEventsUri, null, null, null, null);
1373        assertEquals(0, cursor.getCount());
1374        cursor.close();
1375
1376        // Keep track of the number of repeating events
1377        int numEvents = 0;
1378
1379        int len = mEvents.length;
1380        for (int ii = 0; ii < len; ii++) {
1381            EventInfo event = mEvents[ii];
1382            // Skip normal events
1383            if (event.mRrule == null) {
1384                continue;
1385            }
1386            url = insertEvent(calId, event);
1387            numEvents += 1;
1388        }
1389
1390        // query one
1391        cursor = mResolver.query(url, null, null, null, null);
1392        assertEquals(1, cursor.getCount());
1393        cursor.close();
1394
1395        // query all
1396        cursor = mResolver.query(mEventsUri, null, null, null, null);
1397        assertEquals(numEvents, cursor.getCount());
1398        cursor.close();
1399    }
1400
1401    // Force a dtend value to be set and make sure instance expansion still works
1402    public void testInstanceRangeDtend() throws Exception {
1403        mForceDtend = true;
1404        testInstanceRange();
1405    }
1406
1407    public void testInstanceRange() throws Exception {
1408        Cursor cursor;
1409        Uri url = null;
1410
1411        int calId = insertCal("Calendar0", "America/Los_Angeles");
1412
1413        cursor = mResolver.query(mEventsUri, null, null, null, null);
1414        assertEquals(0, cursor.getCount());
1415        cursor.close();
1416
1417        int len = mInstanceRanges.length;
1418        for (int ii = 0; ii < len; ii++) {
1419            InstanceInfo instance = mInstanceRanges[ii];
1420            EventInfo event = instance.mEvent;
1421            url = insertEvent(calId, event);
1422            cursor = queryInstances(instance.mBegin, instance.mEnd);
1423            if (instance.mExpectedOccurrences != cursor.getCount()) {
1424                Log.e(TAG, "Test failed! Instance index: " + ii);
1425                Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription
1426                        + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]"
1427                        + " expected: " + instance.mExpectedOccurrences);
1428                dumpCursor(cursor);
1429            }
1430            assertEquals(instance.mExpectedOccurrences, cursor.getCount());
1431            cursor.close();
1432            // Delete as sync_adapter so event is really deleted.
1433            int rows = mResolver.delete(
1434                    updatedUri(url, true, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
1435                    null /* selection */, null /* selection args */);
1436            assertEquals(1, rows);
1437        }
1438    }
1439
1440    public static <T> void assertArrayEquals(T[] expected, T[] actual) {
1441        if (!Arrays.equals(expected, actual)) {
1442            fail("expected:<" + Arrays.toString(expected) +
1443                "> but was:<" + Arrays.toString(actual) + ">");
1444        }
1445    }
1446
1447    @SmallTest @Smoke
1448    public void testEscapeSearchToken() {
1449        String token = "test";
1450        String expected = "test";
1451        assertEquals(expected, mProvider.escapeSearchToken(token));
1452
1453        token = "%";
1454        expected = "#%";
1455        assertEquals(expected, mProvider.escapeSearchToken(token));
1456
1457        token = "_";
1458        expected = "#_";
1459        assertEquals(expected, mProvider.escapeSearchToken(token));
1460
1461        token = "#";
1462        expected = "##";
1463        assertEquals(expected, mProvider.escapeSearchToken(token));
1464
1465        token = "##";
1466        expected = "####";
1467        assertEquals(expected, mProvider.escapeSearchToken(token));
1468
1469        token = "%_#";
1470        expected = "#%#_##";
1471        assertEquals(expected, mProvider.escapeSearchToken(token));
1472
1473        token = "blah%blah";
1474        expected = "blah#%blah";
1475        assertEquals(expected, mProvider.escapeSearchToken(token));
1476    }
1477
1478    @SmallTest @Smoke
1479    public void testTokenizeSearchQuery() {
1480        String query = "";
1481        String[] expectedTokens = new String[] {};
1482        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1483
1484        query = "a";
1485        expectedTokens = new String[] {"a"};
1486        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1487
1488        query = "word";
1489        expectedTokens = new String[] {"word"};
1490        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1491
1492        query = "two words";
1493        expectedTokens = new String[] {"two", "words"};
1494        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1495
1496        query = "test, punctuation.";
1497        expectedTokens = new String[] {"test", "punctuation"};
1498        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1499
1500        query = "\"test phrase\"";
1501        expectedTokens = new String[] {"test phrase"};
1502        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1503
1504        query = "unquoted \"this is quoted\"";
1505        expectedTokens = new String[] {"unquoted", "this is quoted"};
1506        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1507
1508        query = " \"this is quoted\"  unquoted ";
1509        expectedTokens = new String[] {"this is quoted", "unquoted"};
1510        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1511
1512        query = "escap%e m_e";
1513        expectedTokens = new String[] {"escap#%e", "m#_e"};
1514        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1515
1516        query = "'a bunch' of malformed\" things";
1517        expectedTokens = new String[] {"a", "bunch", "of", "malformed", "things"};
1518        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1519
1520        query = "''''''....,.''trim punctuation";
1521        expectedTokens = new String[] {"trim", "punctuation"};
1522        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1523    }
1524
1525    @SmallTest @Smoke
1526    public void testConstructSearchWhere() {
1527        String[] tokens = new String[] {"red"};
1528        String expected = "(title LIKE ? ESCAPE \"#\" OR "
1529            + "description LIKE ? ESCAPE \"#\" OR "
1530            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1531            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1532            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1533        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1534
1535        tokens = new String[] {};
1536        expected = "";
1537        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1538
1539        tokens = new String[] {"red", "green"};
1540        expected = "(title LIKE ? ESCAPE \"#\" OR "
1541                + "description LIKE ? ESCAPE \"#\" OR "
1542                + "eventLocation LIKE ? ESCAPE \"#\" OR "
1543                + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1544                + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1545                + "(title LIKE ? ESCAPE \"#\" OR "
1546                + "description LIKE ? ESCAPE \"#\" OR "
1547                + "eventLocation LIKE ? ESCAPE \"#\" OR "
1548                + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1549                + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1550        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1551
1552        tokens = new String[] {"red blue", "green"};
1553        expected = "(title LIKE ? ESCAPE \"#\" OR "
1554            + "description LIKE ? ESCAPE \"#\" OR "
1555            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1556            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1557            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1558            + "(title LIKE ? ESCAPE \"#\" OR "
1559            + "description LIKE ? ESCAPE \"#\" OR "
1560            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1561            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1562            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1563        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1564    }
1565
1566    @SmallTest @Smoke
1567    public void testConstructSearchArgs() {
1568        long rangeBegin = 0;
1569        long rangeEnd = 10;
1570
1571        String[] tokens = new String[] {"red"};
1572        String[] expected = new String[] {"10", "0", "%red%", "%red%",
1573                "%red%", "%red%", "%red%" };
1574        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1575                rangeBegin, rangeEnd));
1576
1577        tokens = new String[] {"red", "blue"};
1578        expected = new String[] { "10", "0", "%red%", "%red%", "%red%",
1579                "%red%", "%red%", "%blue%", "%blue%",
1580                "%blue%", "%blue%","%blue%"};
1581        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1582                rangeBegin, rangeEnd));
1583
1584        tokens = new String[] {};
1585        expected = new String[] {"10", "0" };
1586        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1587                rangeBegin, rangeEnd));
1588    }
1589
1590    public void testInstanceSearchQuery() throws Exception {
1591        final String[] PROJECTION = new String[] {
1592                Instances.TITLE,                 // 0
1593                Instances.EVENT_LOCATION,        // 1
1594                Instances.ALL_DAY,               // 2
1595                Instances.CALENDAR_COLOR,        // 3
1596                Instances.EVENT_TIMEZONE,        // 4
1597                Instances.EVENT_ID,              // 5
1598                Instances.BEGIN,                 // 6
1599                Instances.END,                   // 7
1600                Instances._ID,                   // 8
1601                Instances.START_DAY,             // 9
1602                Instances.END_DAY,               // 10
1603                Instances.START_MINUTE,          // 11
1604                Instances.END_MINUTE,            // 12
1605                Instances.HAS_ALARM,             // 13
1606                Instances.RRULE,                 // 14
1607                Instances.RDATE,                 // 15
1608                Instances.SELF_ATTENDEE_STATUS,  // 16
1609                Events.ORGANIZER,                // 17
1610                Events.GUESTS_CAN_MODIFY,        // 18
1611        };
1612
1613        String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
1614        String where = Instances.SELF_ATTENDEE_STATUS + "!=" +
1615                CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED;
1616
1617        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1618        final String START = "2008-05-01T00:00:00";
1619        final String END = "2008-05-01T20:00:00";
1620
1621        EventInfo event1 = new EventInfo("search orange",
1622                START,
1623                END,
1624                false /* allDay */,
1625                DEFAULT_TIMEZONE);
1626        event1.mDescription = "this is description1";
1627
1628        EventInfo event2 = new EventInfo("search purple",
1629                START,
1630                END,
1631                false /* allDay */,
1632                DEFAULT_TIMEZONE);
1633        event2.mDescription = "lasers, out of nowhere";
1634
1635        EventInfo event3 = new EventInfo("",
1636                START,
1637                END,
1638                false /* allDay */,
1639                DEFAULT_TIMEZONE);
1640        event3.mDescription = "kapow";
1641
1642        EventInfo[] events = { event1, event2, event3 };
1643
1644        insertEvent(calId, events[0]);
1645        insertEvent(calId, events[1]);
1646        insertEvent(calId, events[2]);
1647
1648        Time time = new Time(DEFAULT_TIMEZONE);
1649        time.parse3339(START);
1650        long startMs = time.toMillis(true /* ignoreDst */);
1651        // Query starting from way in the past to one hour into the event.
1652        // Query is more than 2 months so the range won't get extended by the provider.
1653        Cursor cursor = null;
1654
1655        try {
1656            cursor = queryInstances(mResolver, PROJECTION,
1657                    startMs - DateUtils.YEAR_IN_MILLIS,
1658                    startMs + DateUtils.HOUR_IN_MILLIS,
1659                    "search", where, null, orderBy);
1660            assertEquals(2, cursor.getCount());
1661        } finally {
1662            if (cursor != null) {
1663                cursor.close();
1664            }
1665        }
1666
1667        try {
1668            cursor = queryInstances(mResolver, PROJECTION,
1669                    startMs - DateUtils.YEAR_IN_MILLIS,
1670                    startMs + DateUtils.HOUR_IN_MILLIS,
1671                    "purple", where, null, orderBy);
1672            assertEquals(1, cursor.getCount());
1673        } finally {
1674            if (cursor != null) {
1675                cursor.close();
1676            }
1677        }
1678
1679        try {
1680            cursor = queryInstances(mResolver, PROJECTION,
1681                    startMs - DateUtils.YEAR_IN_MILLIS,
1682                    startMs + DateUtils.HOUR_IN_MILLIS,
1683                    "puurple", where, null, orderBy);
1684            assertEquals(0, cursor.getCount());
1685        } finally {
1686            if (cursor != null) {
1687                cursor.close();
1688            }
1689        }
1690
1691        try {
1692            cursor = queryInstances(mResolver, PROJECTION,
1693                    startMs - DateUtils.YEAR_IN_MILLIS,
1694                    startMs + DateUtils.HOUR_IN_MILLIS,
1695                    "purple lasers", where, null, orderBy);
1696            assertEquals(1, cursor.getCount());
1697        } finally {
1698            if (cursor != null) {
1699                cursor.close();
1700            }
1701        }
1702
1703        try {
1704            cursor = queryInstances(mResolver, PROJECTION,
1705                    startMs - DateUtils.YEAR_IN_MILLIS,
1706                    startMs + DateUtils.HOUR_IN_MILLIS,
1707                    "lasers kapow", where, null, orderBy);
1708            assertEquals(0, cursor.getCount());
1709        } finally {
1710            if (cursor != null) {
1711                cursor.close();
1712            }
1713        }
1714
1715        try {
1716            cursor = queryInstances(mResolver, PROJECTION,
1717                    startMs - DateUtils.YEAR_IN_MILLIS,
1718                    startMs + DateUtils.HOUR_IN_MILLIS,
1719                    "\"search purple\"", where, null, orderBy);
1720            assertEquals(1, cursor.getCount());
1721        } finally {
1722            if (cursor != null) {
1723                cursor.close();
1724            }
1725        }
1726
1727        try {
1728            cursor = queryInstances(mResolver, PROJECTION,
1729                    startMs - DateUtils.YEAR_IN_MILLIS,
1730                    startMs + DateUtils.HOUR_IN_MILLIS,
1731                    "\"purple search\"", where, null, orderBy);
1732            assertEquals(0, cursor.getCount());
1733        } finally {
1734            if (cursor != null) {
1735                cursor.close();
1736            }
1737        }
1738
1739        try {
1740            cursor = queryInstances(mResolver, PROJECTION,
1741                    startMs - DateUtils.YEAR_IN_MILLIS,
1742                    startMs + DateUtils.HOUR_IN_MILLIS,
1743                    "%", where, null, orderBy);
1744            assertEquals(0, cursor.getCount());
1745        } finally {
1746            if (cursor != null) {
1747                cursor.close();
1748            }
1749        }
1750    }
1751
1752    public void testDeleteCalendar() throws Exception {
1753        int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
1754        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1755        insertEvent(calendarId0, mEvents[0]);
1756        insertEvent(calendarId1, mEvents[1]);
1757        // Should have 2 calendars and 2 events
1758        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 2);
1759        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 2);
1760
1761        int deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
1762                "ownerAccount='user2@google.com'", null /* selectionArgs */);
1763
1764        assertEquals(1, deletes);
1765        // Should have 1 calendar and 1 event
1766        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 1);
1767        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 1);
1768
1769        deletes = mResolver.delete(Uri.withAppendedPath(CalendarContract.Calendars.CONTENT_URI,
1770                String.valueOf(calendarId0)),
1771                null /* selection*/ , null /* selectionArgs */);
1772
1773        assertEquals(1, deletes);
1774        // Should have 0 calendars and 0 events
1775        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 0);
1776        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 0);
1777
1778        deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
1779                "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */);
1780
1781        assertEquals(0, deletes);
1782    }
1783
1784    public void testCalendarAlerts() throws Exception {
1785        // This projection is from AlertActivity; want to make sure it works.
1786        String[] projection = new String[] {
1787                CalendarContract.CalendarAlerts._ID,              // 0
1788                CalendarContract.CalendarAlerts.TITLE,            // 1
1789                CalendarContract.CalendarAlerts.EVENT_LOCATION,   // 2
1790                CalendarContract.CalendarAlerts.ALL_DAY,          // 3
1791                CalendarContract.CalendarAlerts.BEGIN,            // 4
1792                CalendarContract.CalendarAlerts.END,              // 5
1793                CalendarContract.CalendarAlerts.EVENT_ID,         // 6
1794                CalendarContract.CalendarAlerts.CALENDAR_COLOR,   // 7
1795                CalendarContract.CalendarAlerts.RRULE,            // 8
1796                CalendarContract.CalendarAlerts.HAS_ALARM,        // 9
1797                CalendarContract.CalendarAlerts.STATE,            // 10
1798                CalendarContract.CalendarAlerts.ALARM_TIME,       // 11
1799        };
1800        testInsertNormalEvents(); // To initialize
1801
1802        Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1803                2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
1804        CalendarContract.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1805                2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */);
1806
1807        // Regular query
1808        Cursor cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI, projection,
1809                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1810
1811        assertEquals(2, cursor.getCount());
1812        cursor.close();
1813
1814        // Instance query
1815        cursor = mResolver.query(alertUri, projection,
1816                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1817
1818        assertEquals(1, cursor.getCount());
1819        cursor.close();
1820
1821        // Grouped by event query
1822        cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI_BY_INSTANCE,
1823                projection, null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1824
1825        assertEquals(1, cursor.getCount());
1826        cursor.close();
1827    }
1828
1829    void checkEvents(int count, SQLiteDatabase db) {
1830        Cursor cursor = db.query("Events", null, null, null, null, null, null);
1831        try {
1832            assertEquals(count, cursor.getCount());
1833        } finally {
1834            cursor.close();
1835        }
1836    }
1837
1838    void checkEvents(int count, SQLiteDatabase db, String calendar) {
1839        Cursor cursor = db.query("Events", null, Events.CALENDAR_ID + "=?", new String[] {calendar},
1840                null, null, null);
1841        try {
1842            assertEquals(count, cursor.getCount());
1843        } finally {
1844            cursor.close();
1845        }
1846    }
1847    /**
1848     * Test attendee processing
1849     * @throws Exception
1850     */
1851    public void testAttendees() throws Exception {
1852        mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE);
1853        String calendarIdString = Integer.toString(mCalendarId);
1854        checkEvents(0, mDb, calendarIdString);
1855        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1856        // TODO This has a race condition that causes checkEvents to not find
1857        // the just added event
1858        Thread.sleep(200);
1859        checkEvents(1, mDb, calendarIdString);
1860        long eventId = ContentUris.parseId(eventUri);
1861
1862        ContentValues attendee = new ContentValues();
1863        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe");
1864        attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
1865        attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE,
1866                CalendarContract.Attendees.TYPE_REQUIRED);
1867        attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
1868                CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
1869        attendee.put(CalendarContract.Attendees.EVENT_ID, eventId);
1870        Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee);
1871
1872        Cursor cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null,
1873                "event_id=" + eventId, null, null);
1874        assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
1875                cursor.getCount());
1876        cursor.close();
1877
1878        cursor = mResolver.query(eventUri, null, null, null, null);
1879        // TODO figure out why this test fails. App works fine for this case.
1880        assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
1881                cursor.getCount());
1882        int selfColumn = cursor.getColumnIndex(CalendarContract.Events.SELF_ATTENDEE_STATUS);
1883        cursor.moveToNext();
1884        long selfAttendeeStatus = cursor.getInt(selfColumn);
1885        assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
1886        cursor.close();
1887
1888        // Change status to declined
1889        attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS,
1890                CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED);
1891        mResolver.update(attendeesUri, attendee, null, null);
1892
1893        cursor = mResolver.query(eventUri, null, null, null, null);
1894        cursor.moveToNext();
1895        selfAttendeeStatus = cursor.getInt(selfColumn);
1896        assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1897        cursor.close();
1898
1899        // Add another attendee
1900        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Dude");
1901        attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
1902        attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS,
1903                CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED);
1904        mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee);
1905
1906        cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null,
1907                "event_id=" + mCalendarId, null, null);
1908        assertEquals(2, cursor.getCount());
1909        cursor.close();
1910
1911        cursor = mResolver.query(eventUri, null, null, null, null);
1912        cursor.moveToNext();
1913        selfAttendeeStatus = cursor.getInt(selfColumn);
1914        assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1915        cursor.close();
1916    }
1917
1918    /**
1919     * Test the event's dirty status and clear it.
1920     *
1921     * @param eventId event to fetch.
1922     * @param wanted the wanted dirty status
1923     */
1924    private void testAndClearDirty(long eventId, int wanted) {
1925        Cursor cursor = mResolver.query(
1926                ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId),
1927                null, null, null, null);
1928        try {
1929            assertEquals("Event count", 1, cursor.getCount());
1930            cursor.moveToNext();
1931            int dirty = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.DIRTY));
1932            assertEquals("dirty flag", wanted, dirty);
1933            if (dirty == 1) {
1934                // Have to access database directly since provider will set dirty again.
1935                mDb.execSQL("UPDATE Events SET " + Events.DIRTY + "=0 WHERE _id=" + eventId);
1936            }
1937        } finally {
1938            cursor.close();
1939        }
1940    }
1941
1942    /**
1943     * Test the count of results from a query.
1944     * @param uri The URI to query
1945     * @param where The where string or null.
1946     * @param wanted The number of results wanted.  An assertion is thrown if it doesn't match.
1947     */
1948    private void testQueryCount(Uri uri, String where, int wanted) {
1949        Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */,
1950                null /* sortOrder */);
1951        try {
1952            assertEquals("query results", wanted, cursor.getCount());
1953        } finally {
1954            cursor.close();
1955        }
1956    }
1957
1958    /**
1959     * Test dirty flag processing.
1960     * @throws Exception
1961     */
1962    public void testDirty() throws Exception {
1963        internalTestDirty(false);
1964    }
1965
1966    /**
1967     * Test dirty flag processing for updates from a sync adapter.
1968     * @throws Exception
1969     */
1970    public void testDirtyWithSyncAdapter() throws Exception {
1971        internalTestDirty(true);
1972    }
1973
1974    /**
1975     * Adds CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.  Otherwise,
1976     * returns the original URI.
1977     */
1978    private Uri updatedUri(Uri uri, boolean syncAdapter, String account, String accountType) {
1979        if (syncAdapter) {
1980            return addSyncQueryParams(uri, account, accountType);
1981        } else {
1982            return uri;
1983        }
1984    }
1985
1986    /**
1987     * Test dirty flag processing either for syncAdapter operations or client operations.
1988     * The main difference is syncAdapter operations don't set the dirty bit.
1989     */
1990    private void internalTestDirty(boolean syncAdapter) throws Exception {
1991        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1992
1993        long now = System.currentTimeMillis();
1994        long begin = (now / 1000) * 1000;
1995        long end = begin + ONE_HOUR_MILLIS;
1996        Time time = new Time(DEFAULT_TIMEZONE);
1997        time.set(begin);
1998        String startDate = time.format3339(false);
1999        time.set(end);
2000        String endDate = time.format3339(false);
2001
2002        EventInfo eventInfo = new EventInfo("current", startDate, endDate, false);
2003        Uri eventUri = insertEvent(mCalendarId, eventInfo);
2004
2005        long eventId = ContentUris.parseId(eventUri);
2006        testAndClearDirty(eventId, 1);
2007
2008        ContentValues attendee = new ContentValues();
2009        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe");
2010        attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
2011        attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE,
2012                CalendarContract.Attendees.TYPE_REQUIRED);
2013        attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
2014                CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
2015        attendee.put(CalendarContract.Attendees.EVENT_ID, eventId);
2016
2017        Uri attendeeUri = mResolver.insert(
2018                updatedUri(CalendarContract.Attendees.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2019                        DEFAULT_ACCOUNT_TYPE),
2020                attendee);
2021        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2022        testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2023
2024        ContentValues reminder = new ContentValues();
2025        reminder.put(CalendarContract.Reminders.MINUTES, 30);
2026        reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL);
2027        reminder.put(CalendarContract.Attendees.EVENT_ID, eventId);
2028
2029        Uri reminderUri = mResolver.insert(
2030                updatedUri(CalendarContract.Reminders.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2031                        DEFAULT_ACCOUNT_TYPE), reminder);
2032        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2033        testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
2034
2035        long alarmTime = begin + 5 * ONE_MINUTE_MILLIS;
2036
2037        ContentValues alert = new ContentValues();
2038        alert.put(CalendarContract.CalendarAlerts.BEGIN, begin);
2039        alert.put(CalendarContract.CalendarAlerts.END, end);
2040        alert.put(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime);
2041        alert.put(CalendarContract.CalendarAlerts.CREATION_TIME, now);
2042        alert.put(CalendarContract.CalendarAlerts.RECEIVED_TIME, now);
2043        alert.put(CalendarContract.CalendarAlerts.NOTIFY_TIME, now);
2044        alert.put(CalendarContract.CalendarAlerts.STATE,
2045                CalendarContract.CalendarAlerts.STATE_SCHEDULED);
2046        alert.put(CalendarContract.CalendarAlerts.MINUTES, 30);
2047        alert.put(CalendarContract.CalendarAlerts.EVENT_ID, eventId);
2048
2049        Uri alertUri = mResolver.insert(
2050                updatedUri(CalendarContract.CalendarAlerts.CONTENT_URI, syncAdapter,
2051                        DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert);
2052        // Alerts don't dirty the event
2053        testAndClearDirty(eventId, 0);
2054        testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2055
2056        ContentValues extended = new ContentValues();
2057        extended.put(CalendarContract.ExtendedProperties.NAME, "foo");
2058        extended.put(CalendarContract.ExtendedProperties.VALUE, "bar");
2059        extended.put(CalendarContract.ExtendedProperties.EVENT_ID, eventId);
2060
2061        Uri extendedUri = null;
2062        if (syncAdapter) {
2063            // Only the sync adapter is allowed to modify ExtendedProperties.
2064            extendedUri = mResolver.insert(
2065                    updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter,
2066                            DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended);
2067            testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2068            testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI,
2069                    "event_id=" + eventId, 2);
2070        } else {
2071            // Confirm that inserting as app fails.
2072            try {
2073                extendedUri = mResolver.insert(
2074                        updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter,
2075                                DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended);
2076                fail("Only sync adapter should be allowed to insert into ExtendedProperties");
2077            } catch (IllegalArgumentException iae) {}
2078        }
2079
2080        // Now test updates
2081
2082        attendee = new ContentValues();
2083        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Sam");
2084
2085        assertEquals("update", 1, mResolver.update(
2086                updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2087                attendee,
2088                null /* where */, null /* selectionArgs */));
2089        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2090
2091        testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2092
2093        alert = new ContentValues();
2094        alert.put(CalendarContract.CalendarAlerts.STATE,
2095                CalendarContract.CalendarAlerts.STATE_DISMISSED);
2096
2097        assertEquals("update", 1, mResolver.update(
2098                updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert,
2099                null /* where */, null /* selectionArgs */));
2100        // Alerts don't dirty the event
2101        testAndClearDirty(eventId, 0);
2102        testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2103
2104        extended = new ContentValues();
2105        extended.put(CalendarContract.ExtendedProperties.VALUE, "baz");
2106
2107        if (syncAdapter) {
2108            assertEquals("update", 1, mResolver.update(
2109                    updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2110                    extended,
2111                    null /* where */, null /* selectionArgs */));
2112            testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2113            testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI,
2114                    "event_id=" + eventId, 2);
2115        }
2116
2117        // Now test deletes
2118
2119        assertEquals("delete", 1, mResolver.delete(
2120                updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2121                null, null /* selectionArgs */));
2122        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2123        testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
2124
2125        assertEquals("delete", 1, mResolver.delete(
2126                updatedUri(reminderUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2127                null /* where */, null /* selectionArgs */));
2128
2129        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2130        testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
2131
2132        assertEquals("delete", 1, mResolver.delete(
2133                updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2134                null /* where */, null /* selectionArgs */));
2135
2136        // Alerts don't dirty the event
2137        testAndClearDirty(eventId, 0);
2138        testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
2139
2140        if (syncAdapter) {
2141            assertEquals("delete", 1, mResolver.delete(
2142                    updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2143                    null /* where */, null /* selectionArgs */));
2144
2145            testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2146            testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
2147        }
2148    }
2149
2150    /**
2151     * Test calendar deletion
2152     * @throws Exception
2153     */
2154    public void testCalendarDeletion() throws Exception {
2155        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2156        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
2157        long eventId = ContentUris.parseId(eventUri);
2158        testAndClearDirty(eventId, 1);
2159        Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1"));
2160        long eventId1 = ContentUris.parseId(eventUri);
2161        assertEquals("delete", 1, mResolver.delete(eventUri1, null, null));
2162        // Calendar has one event and one deleted event
2163        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2164
2165        assertEquals("delete", 1, mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
2166                "_id=" + mCalendarId, null));
2167        // Calendar should be deleted
2168        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null, 0);
2169        // Event should be gone
2170        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 0);
2171    }
2172
2173    /**
2174     * Test multiple account support.
2175     */
2176    public void testMultipleAccounts() throws Exception {
2177        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2178        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
2179        Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0"));
2180        Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"));
2181
2182        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2183        Uri eventsWithAccount = CalendarContract.Events.CONTENT_URI.buildUpon()
2184                .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_NAME, DEFAULT_ACCOUNT)
2185                .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_TYPE,
2186                        DEFAULT_ACCOUNT_TYPE)
2187                .build();
2188        // Only one event for that account
2189        testQueryCount(eventsWithAccount, null, 1);
2190
2191        // Test deletion with account and selection
2192
2193        long eventId = ContentUris.parseId(eventUri1);
2194        // Wrong account, should not be deleted
2195        assertEquals("delete", 0, mResolver.delete(
2196                updatedUri(eventsWithAccount, true /* syncAdapter */, DEFAULT_ACCOUNT,
2197                        DEFAULT_ACCOUNT_TYPE),
2198                "_id=" + eventId, null /* selectionArgs */));
2199        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2200        // Right account, should be deleted
2201        assertEquals("delete", 1, mResolver.delete(
2202                updatedUri(CalendarContract.Events.CONTENT_URI, true /* syncAdapter */,
2203                        "user2@google.com", DEFAULT_ACCOUNT_TYPE),
2204                "_id=" + eventId, null /* selectionArgs */));
2205        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 1);
2206    }
2207
2208    /**
2209     * Run commands, wiping instance table at each step.
2210     * This tests full instance expansion.
2211     * @throws Exception
2212     */
2213    public void testCommandSequences1() throws Exception {
2214        commandSequences(true);
2215    }
2216
2217    /**
2218     * Run commands normally.
2219     * This tests incremental instance expansion.
2220     * @throws Exception
2221     */
2222    public void testCommandSequences2() throws Exception {
2223        commandSequences(false);
2224    }
2225
2226    /**
2227     * Run thorough set of command sequences
2228     * @param wipe true if instances should be wiped and regenerated
2229     * @throws Exception
2230     */
2231    private void commandSequences(boolean wipe) throws Exception {
2232        Cursor cursor;
2233        Uri url = null;
2234        mWipe = wipe; // Set global flag
2235
2236        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2237
2238        cursor = mResolver.query(mEventsUri, null, null, null, null);
2239        assertEquals(0, cursor.getCount());
2240        cursor.close();
2241        Command[] commands;
2242
2243        Log.i(TAG, "Normal insert/delete");
2244        commands = mNormalInsertDelete;
2245        for (Command command : commands) {
2246            command.execute();
2247        }
2248
2249        deleteAllEvents();
2250
2251        Log.i(TAG, "All-day insert/delete");
2252        commands = mAlldayInsertDelete;
2253        for (Command command : commands) {
2254            command.execute();
2255        }
2256
2257        deleteAllEvents();
2258
2259        Log.i(TAG, "Recurring insert/delete");
2260        commands = mRecurringInsertDelete;
2261        for (Command command : commands) {
2262            command.execute();
2263        }
2264
2265        deleteAllEvents();
2266
2267        Log.i(TAG, "Exception with truncated recurrence");
2268        commands = mExceptionWithTruncatedRecurrence;
2269        for (Command command : commands) {
2270            command.execute();
2271        }
2272
2273        deleteAllEvents();
2274
2275        Log.i(TAG, "Exception with moved recurrence");
2276        commands = mExceptionWithMovedRecurrence;
2277        for (Command command : commands) {
2278            command.execute();
2279        }
2280
2281        deleteAllEvents();
2282
2283        Log.i(TAG, "Exception with cancel");
2284        commands = mCancelInstance;
2285        for (Command command : commands) {
2286            command.execute();
2287        }
2288
2289        deleteAllEvents();
2290
2291        Log.i(TAG, "Exception with moved recurrence2");
2292        commands = mExceptionWithMovedRecurrence2;
2293        for (Command command : commands) {
2294            command.execute();
2295        }
2296
2297        deleteAllEvents();
2298
2299        Log.i(TAG, "Exception with no recurrence");
2300        commands = mExceptionWithNoRecurrence;
2301        for (Command command : commands) {
2302            command.execute();
2303        }
2304    }
2305
2306    /**
2307     * Test Time toString.
2308     * @throws Exception
2309     */
2310    // Suppressed because toString currently hangs.
2311    @Suppress
2312    public void testTimeToString() throws Exception {
2313        Time time = new Time(Time.TIMEZONE_UTC);
2314        String str = "2039-01-01T23:00:00.000Z";
2315        String result = "20390101T230000UTC(0,0,0,-1,0)";
2316        time.parse3339(str);
2317        assertEquals(result, time.toString());
2318    }
2319
2320    /**
2321     * Test the query done by Event.loadEvents
2322     * Also test that instance queries work when an event straddles the expansion range
2323     * @throws Exception
2324     */
2325    public void testInstanceQuery() throws Exception {
2326        final String[] PROJECTION = new String[] {
2327                Instances.TITLE,                 // 0
2328                Instances.EVENT_LOCATION,        // 1
2329                Instances.ALL_DAY,               // 2
2330                Instances.CALENDAR_COLOR,        // 3
2331                Instances.EVENT_TIMEZONE,        // 4
2332                Instances.EVENT_ID,              // 5
2333                Instances.BEGIN,                 // 6
2334                Instances.END,                   // 7
2335                Instances._ID,                   // 8
2336                Instances.START_DAY,             // 9
2337                Instances.END_DAY,               // 10
2338                Instances.START_MINUTE,          // 11
2339                Instances.END_MINUTE,            // 12
2340                Instances.HAS_ALARM,             // 13
2341                Instances.RRULE,                 // 14
2342                Instances.RDATE,                 // 15
2343                Instances.SELF_ATTENDEE_STATUS,  // 16
2344                Events.ORGANIZER,                // 17
2345                Events.GUESTS_CAN_MODIFY,        // 18
2346        };
2347
2348        String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
2349        String where = Instances.SELF_ATTENDEE_STATUS + "!="
2350                + CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED;
2351
2352        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2353        final String START = "2008-05-01T00:00:00";
2354        final String END = "2008-05-01T20:00:00";
2355
2356        EventInfo[] events = { new EventInfo("normal0",
2357                START,
2358                END,
2359                false /* allDay */,
2360                DEFAULT_TIMEZONE) };
2361
2362        insertEvent(calId, events[0]);
2363
2364        Time time = new Time(DEFAULT_TIMEZONE);
2365        time.parse3339(START);
2366        long startMs = time.toMillis(true /* ignoreDst */);
2367        // Query starting from way in the past to one hour into the event.
2368        // Query is more than 2 months so the range won't get extended by the provider.
2369        Cursor cursor = queryInstances(mResolver, PROJECTION,
2370                startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS,
2371                where, null, orderBy);
2372        try {
2373            assertEquals(1, cursor.getCount());
2374        } finally {
2375            cursor.close();
2376        }
2377
2378        // Now expand the instance range.  The event overlaps the new part of the range.
2379        cursor = queryInstances(mResolver, PROJECTION,
2380                startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS,
2381                where, null, orderBy);
2382        try {
2383            assertEquals(1, cursor.getCount());
2384        } finally {
2385            cursor.close();
2386        }
2387    }
2388
2389    /**
2390     * Performs a query to return all visible instances in the given range that
2391     * match the given selection. This is a blocking function and should not be
2392     * done on the UI thread. This will cause an expansion of recurring events
2393     * to fill this time range if they are not already expanded and will slow
2394     * down for larger time ranges with many recurring events.
2395     *
2396     * @param cr The ContentResolver to use for the query
2397     * @param projection The columns to return
2398     * @param begin The start of the time range to query in UTC millis since
2399     *            epoch
2400     * @param end The end of the time range to query in UTC millis since epoch
2401     * @param selection Filter on the query as an SQL WHERE statement
2402     * @param selectionArgs Args to replace any '?'s in the selection
2403     * @param orderBy How to order the rows as an SQL ORDER BY statement
2404     * @return A Cursor of instances matching the selection
2405     */
2406    private static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin,
2407            long end, String selection, String[] selectionArgs, String orderBy) {
2408
2409        Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
2410        ContentUris.appendId(builder, begin);
2411        ContentUris.appendId(builder, end);
2412        if (TextUtils.isEmpty(selection)) {
2413            selection = WHERE_CALENDARS_SELECTED;
2414            selectionArgs = WHERE_CALENDARS_ARGS;
2415        } else {
2416            selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED;
2417            if (selectionArgs != null && selectionArgs.length > 0) {
2418                selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1);
2419                selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0];
2420            } else {
2421                selectionArgs = WHERE_CALENDARS_ARGS;
2422            }
2423        }
2424        return cr.query(builder.build(), projection, selection, selectionArgs,
2425                orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
2426    }
2427
2428    /**
2429     * Performs a query to return all visible instances in the given range that
2430     * match the given selection. This is a blocking function and should not be
2431     * done on the UI thread. This will cause an expansion of recurring events
2432     * to fill this time range if they are not already expanded and will slow
2433     * down for larger time ranges with many recurring events.
2434     *
2435     * @param cr The ContentResolver to use for the query
2436     * @param projection The columns to return
2437     * @param begin The start of the time range to query in UTC millis since
2438     *            epoch
2439     * @param end The end of the time range to query in UTC millis since epoch
2440     * @param searchQuery A string of space separated search terms. Segments
2441     *            enclosed by double quotes will be treated as a single term.
2442     * @param selection Filter on the query as an SQL WHERE statement
2443     * @param selectionArgs Args to replace any '?'s in the selection
2444     * @param orderBy How to order the rows as an SQL ORDER BY statement
2445     * @return A Cursor of instances matching the selection
2446     */
2447    public static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin,
2448            long end, String searchQuery, String selection, String[] selectionArgs, String orderBy)
2449            {
2450        Uri.Builder builder = Instances.CONTENT_SEARCH_URI.buildUpon();
2451        ContentUris.appendId(builder, begin);
2452        ContentUris.appendId(builder, end);
2453        builder = builder.appendPath(searchQuery);
2454        if (TextUtils.isEmpty(selection)) {
2455            selection = WHERE_CALENDARS_SELECTED;
2456            selectionArgs = WHERE_CALENDARS_ARGS;
2457        } else {
2458            selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED;
2459            if (selectionArgs != null && selectionArgs.length > 0) {
2460                selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1);
2461                selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0];
2462            } else {
2463                selectionArgs = WHERE_CALENDARS_ARGS;
2464            }
2465        }
2466        return cr.query(builder.build(), projection, selection, selectionArgs,
2467                orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
2468    }
2469
2470    private Cursor queryInstances(long begin, long end) {
2471        Uri url = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI, begin + "/" + end);
2472        return mResolver.query(url, null, null, null, null);
2473    }
2474
2475    protected static class MockProvider extends ContentProvider {
2476
2477        private String mAuthority;
2478
2479        private int mNumItems = 0;
2480
2481        public MockProvider(String authority) {
2482            mAuthority = authority;
2483        }
2484
2485        @Override
2486        public boolean onCreate() {
2487            return true;
2488        }
2489
2490        @Override
2491        public Cursor query(Uri uri, String[] projection, String selection,
2492                String[] selectionArgs, String sortOrder) {
2493            return new MatrixCursor(new String[]{ "_id" }, 0);
2494        }
2495
2496        @Override
2497        public String getType(Uri uri) {
2498            throw new UnsupportedOperationException();
2499        }
2500
2501        @Override
2502        public Uri insert(Uri uri, ContentValues values) {
2503            mNumItems++;
2504            return Uri.parse("content://" + mAuthority + "/" + mNumItems);
2505        }
2506
2507        @Override
2508        public int delete(Uri uri, String selection, String[] selectionArgs) {
2509            return 0;
2510        }
2511
2512        @Override
2513        public int update(Uri uri, ContentValues values, String selection,
2514                String[] selectionArgs) {
2515            return 0;
2516        }
2517    }
2518
2519    private void cleanCalendarDataTable(SQLiteOpenHelper helper) {
2520        if (null == helper) {
2521            return;
2522        }
2523        SQLiteDatabase db = helper.getWritableDatabase();
2524        db.execSQL("DELETE FROM CalendarCache;");
2525    }
2526
2527    public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException {
2528        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2529        cleanCalendarDataTable(helper);
2530        CalendarCache cache = new CalendarCache(helper);
2531
2532        boolean hasException = false;
2533        try {
2534            String value = cache.readData(null);
2535        } catch (CalendarCache.CacheException e) {
2536            hasException = true;
2537        }
2538        assertTrue(hasException);
2539
2540        assertNull(cache.readTimezoneDatabaseVersion());
2541
2542        cache.writeTimezoneDatabaseVersion("1234");
2543        assertEquals("1234", cache.readTimezoneDatabaseVersion());
2544
2545        cache.writeTimezoneDatabaseVersion("5678");
2546        assertEquals("5678", cache.readTimezoneDatabaseVersion());
2547    }
2548
2549    private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) {
2550        Uri uri = Uri.parse("content://" + CalendarContract.AUTHORITY + "/events");
2551        Log.i(TAG, "Looking for EventId = " + eventId);
2552
2553        Cursor cursor = mResolver.query(uri, null, null, null, null);
2554        assertEquals(1, cursor.getCount());
2555
2556        int colIndexTitle = cursor.getColumnIndex(CalendarContract.Events.TITLE);
2557        int colIndexDtStart = cursor.getColumnIndex(CalendarContract.Events.DTSTART);
2558        int colIndexDtEnd = cursor.getColumnIndex(CalendarContract.Events.DTEND);
2559        int colIndexAllDay = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY);
2560        if (!cursor.moveToNext()) {
2561            Log.e(TAG,"Could not find inserted event");
2562            assertTrue(false);
2563        }
2564        assertEquals(title, cursor.getString(colIndexTitle));
2565        assertEquals(dtStart, cursor.getLong(colIndexDtStart));
2566        assertEquals(dtEnd, cursor.getLong(colIndexDtEnd));
2567        assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0));
2568        cursor.close();
2569    }
2570
2571    public void testChangeTimezoneDB() {
2572        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2573
2574        Cursor cursor = mResolver
2575                .query(CalendarContract.Events.CONTENT_URI, null, null, null, null);
2576        assertEquals(0, cursor.getCount());
2577        cursor.close();
2578
2579        EventInfo[] events = { new EventInfo("normal0",
2580                                        "2008-05-01T00:00:00",
2581                                        "2008-05-02T00:00:00",
2582                                        false,
2583                                        DEFAULT_TIMEZONE) };
2584
2585        Uri uri = insertEvent(calId, events[0]);
2586        assertNotNull(uri);
2587
2588        // check the inserted event
2589        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2590
2591        // inject a new time zone
2592        getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE,
2593                MOCK_TIME_ZONE_DATABASE_VERSION);
2594
2595        // check timezone database version
2596        assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion());
2597
2598        // check that the inserted event has *not* been updated
2599        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2600    }
2601
2602    public static final Uri PROPERTIES_CONTENT_URI =
2603            Uri.parse("content://" + CalendarContract.AUTHORITY + "/properties");
2604
2605    public static final int COLUMN_KEY_INDEX = 1;
2606    public static final int COLUMN_VALUE_INDEX = 0;
2607
2608    public void testGetProviderProperties() throws CalendarCache.CacheException {
2609        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2610        cleanCalendarDataTable(helper);
2611        CalendarCache cache = new CalendarCache(helper);
2612
2613        cache.writeTimezoneDatabaseVersion("2010k");
2614        cache.writeTimezoneInstances("America/Denver");
2615        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2616        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2617
2618        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, null, null, null);
2619        assertEquals(4, cursor.getCount());
2620
2621        assertEquals(CalendarCache.COLUMN_NAME_KEY, cursor.getColumnName(COLUMN_KEY_INDEX));
2622        assertEquals(CalendarCache.COLUMN_NAME_VALUE, cursor.getColumnName(COLUMN_VALUE_INDEX));
2623
2624        Map<String, String> map = new HashMap<String, String>();
2625
2626        while (cursor.moveToNext()) {
2627            String key = cursor.getString(COLUMN_KEY_INDEX);
2628            String value = cursor.getString(COLUMN_VALUE_INDEX);
2629            map.put(key, value);
2630        }
2631
2632        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2633        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_TYPE));
2634        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES));
2635        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2636
2637        assertEquals("2010k", map.get(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2638        assertEquals("America/Denver", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES));
2639        assertEquals("America/Los_Angeles", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2640        assertEquals(CalendarCache.TIMEZONE_TYPE_AUTO, map.get(CalendarCache.KEY_TIMEZONE_TYPE));
2641
2642        cursor.close();
2643    }
2644
2645    public void testGetProviderPropertiesByKey() throws CalendarCache.CacheException {
2646        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2647        cleanCalendarDataTable(helper);
2648        CalendarCache cache = new CalendarCache(helper);
2649
2650        cache.writeTimezoneDatabaseVersion("2010k");
2651        cache.writeTimezoneInstances("America/Denver");
2652        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2653        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2654
2655        checkValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2656        checkValueForKey("2010k", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2657        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2658        checkValueForKey("America/Los_Angeles", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2659    }
2660
2661    private void checkValueForKey(String value, String key) {
2662        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null,
2663                "key=?", new String[] {key}, null);
2664
2665        assertEquals(1, cursor.getCount());
2666        assertTrue(cursor.moveToFirst());
2667        assertEquals(cursor.getString(COLUMN_KEY_INDEX), key);
2668        assertEquals(cursor.getString(COLUMN_VALUE_INDEX), value);
2669
2670        cursor.close();
2671    }
2672
2673    public void testUpdateProviderProperties() throws CalendarCache.CacheException {
2674        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2675        cleanCalendarDataTable(helper);
2676        CalendarCache cache = new CalendarCache(helper);
2677
2678        String localTimezone = TimeZone.getDefault().getID();
2679
2680        // Set initial value
2681        cache.writeTimezoneDatabaseVersion("2010k");
2682
2683        updateValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2684        checkValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2685
2686        // Set initial values
2687        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2688        cache.writeTimezoneInstances("America/Chicago");
2689        cache.writeTimezoneInstancesPrevious("America/Denver");
2690
2691        updateValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2692        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2693        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2694
2695        updateValueForKey(CalendarCache.TIMEZONE_TYPE_HOME, CalendarCache.KEY_TIMEZONE_TYPE);
2696        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2697        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2698
2699        // Set initial value
2700        cache.writeTimezoneInstancesPrevious("");
2701        updateValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2702        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2703        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2704    }
2705
2706    private void updateValueForKey(String value, String key) {
2707        ContentValues contentValues = new ContentValues();
2708        contentValues.put(CalendarCache.COLUMN_NAME_VALUE, value);
2709
2710        int result = mResolver.update(PROPERTIES_CONTENT_URI,
2711                contentValues,
2712                CalendarCache.COLUMN_NAME_KEY + "=?",
2713                new String[] {key});
2714
2715        assertEquals(1, result);
2716    }
2717
2718    public void testInsertOriginalTimezoneInExtProperties() throws Exception {
2719        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2720
2721
2722        EventInfo[] events = { new EventInfo("normal0",
2723                                        "2008-05-01T00:00:00",
2724                                        "2008-05-02T00:00:00",
2725                                        false,
2726                                        DEFAULT_TIMEZONE) };
2727
2728        Uri eventUri = insertEvent(calId, events[0]);
2729        assertNotNull(eventUri);
2730
2731        long eventId = ContentUris.parseId(eventUri);
2732        assertTrue(eventId > -1);
2733
2734        // check the inserted event
2735        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2736
2737        // Should have 1 calendars and 1 event
2738        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 1);
2739        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 1);
2740
2741        // Verify that the original timezone is correct
2742        Cursor cursor = mResolver.query(CalendarContract.ExtendedProperties.CONTENT_URI,
2743                null/* projection */,
2744                "event_id=" + eventId,
2745                null /* selectionArgs */,
2746                null /* sortOrder */);
2747        try {
2748            // Should have 1 extended property for the original timezone
2749            assertEquals(1, cursor.getCount());
2750
2751            if (cursor.moveToFirst()) {
2752                long id = cursor.getLong(1);
2753                assertEquals(id, eventId);
2754
2755                assertEquals(CalendarProvider2.EXT_PROP_ORIGINAL_TIMEZONE, cursor.getString(2));
2756                assertEquals(DEFAULT_TIMEZONE, cursor.getString(3));
2757            }
2758        } finally {
2759            cursor.close();
2760        }
2761    }
2762
2763    /**
2764     * Verifies that the number of defined calendars meets expectations.
2765     *
2766     * @param expectedCount The number of calendars we expect to find.
2767     */
2768    private void checkCalendarCount(int expectedCount) {
2769        Cursor cursor = mResolver.query(mCalendarsUri,
2770                null /* projection */,
2771                null /* selection */,
2772                null /* selectionArgs */,
2773                null /* sortOrder */);
2774        assertEquals(expectedCount, cursor.getCount());
2775        cursor.close();
2776    }
2777
2778    private void checkCalendarExists(int calId) {
2779        assertTrue(isCalendarExists(calId));
2780    }
2781
2782    private void checkCalendarDoesNotExists(int calId) {
2783        assertFalse(isCalendarExists(calId));
2784    }
2785
2786    private boolean isCalendarExists(int calId) {
2787        Cursor cursor = mResolver.query(mCalendarsUri,
2788                new String[] {Calendars._ID},
2789                null /* selection */,
2790                null /* selectionArgs */,
2791                null /* sortOrder */);
2792        boolean found = false;
2793        while (cursor.moveToNext()) {
2794            if (calId == cursor.getInt(0)) {
2795                found = true;
2796                break;
2797            }
2798        }
2799        cursor.close();
2800        return found;
2801    }
2802
2803    public void testDeleteAllCalendars() {
2804        checkCalendarCount(0);
2805
2806        insertCal("Calendar1", "America/Los_Angeles");
2807        insertCal("Calendar2", "America/Los_Angeles");
2808
2809        checkCalendarCount(2);
2810
2811        deleteMatchingCalendars(null /* selection */, null /* selectionArgs*/);
2812        checkCalendarCount(0);
2813    }
2814
2815    public void testDeleteCalendarsWithSelection() {
2816        checkCalendarCount(0);
2817
2818        int calId1 = insertCal("Calendar1", "America/Los_Angeles");
2819        int calId2 = insertCal("Calendar2", "America/Los_Angeles");
2820
2821        checkCalendarCount(2);
2822        checkCalendarExists(calId1);
2823        checkCalendarExists(calId2);
2824
2825        deleteMatchingCalendars(Calendars._ID + "=" + calId2, null /* selectionArgs*/);
2826        checkCalendarCount(1);
2827        checkCalendarExists(calId1);
2828        checkCalendarDoesNotExists(calId2);
2829    }
2830
2831    public void testDeleteCalendarsWithSelectionAndArgs() {
2832        checkCalendarCount(0);
2833
2834        int calId1 = insertCal("Calendar1", "America/Los_Angeles");
2835        int calId2 = insertCal("Calendar2", "America/Los_Angeles");
2836
2837        checkCalendarCount(2);
2838        checkCalendarExists(calId1);
2839        checkCalendarExists(calId2);
2840
2841        deleteMatchingCalendars(Calendars._ID + "=?",
2842                new String[] { Integer.toString(calId2) });
2843        checkCalendarCount(1);
2844        checkCalendarExists(calId1);
2845        checkCalendarDoesNotExists(calId2);
2846
2847        deleteMatchingCalendars(Calendars._ID + "=?" + " AND " + Calendars.NAME + "=?",
2848                new String[] { Integer.toString(calId1), "Calendar1" });
2849        checkCalendarCount(0);
2850    }
2851}
2852