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