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