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