CalendarProvider2Test.java revision 02f97c538fc46a08d857d2c807c76fd0eec12493
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.Events.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 verify the events generated by mEvents.  It checks 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.Events.STATUS,
820                        Integer.toString(Calendar.Events.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.Events.STATUS,
883                        Integer.toString(Calendar.Events.STATUS_CANCELED)),
884            }),
885            // Verify that the recurrence exception does not appear.
886            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
887                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}),
888    };
889
890    /**
891     * Bug 135848.  Ensure that a recurrence exception is displayed even if the recurrence
892     * is not present.
893     */
894    private Command[] mExceptionWithNoRecurrence = {
895            new Insert("except0"),
896            new QueryNumEvents(1),
897            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
898                    new String[] {"2008-05-01T02:00:00"}),
899    };
900
901    private EventInfo findEvent(String name) {
902        int len = mEvents.length;
903        for (int ii = 0; ii < len; ii++) {
904            EventInfo event = mEvents[ii];
905            if (name.equals(event.mTitle)) {
906                return event;
907            }
908        }
909        return null;
910    }
911
912    @Override
913    protected void setUp() throws Exception {
914        super.setUp();
915        // This code here is the code that was originally in ProviderTestCase2
916        mResolver = new MockContentResolver();
917
918        final String filenamePrefix = "test.";
919        RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(
920                new MockContext2(), // The context that most methods are delegated to
921                getContext(), // The context that file methods are delegated to
922                filenamePrefix);
923        mContext = new IsolatedContext(mResolver, targetContextWrapper);
924
925        mProvider = new CalendarProvider2ForTesting();
926        mProvider.attachInfo(mContext, null);
927
928        mResolver.addProvider(Calendar.AUTHORITY, mProvider);
929        mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
930        mResolver.addProvider("sync", new MockProvider("sync"));
931
932        mMetaData = getProvider().mMetaData;
933        mForceDtend = false;
934
935        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
936        mDb = helper.getWritableDatabase();
937        wipeAndInitData(helper, mDb);
938    }
939
940    @Override
941    protected void tearDown() throws Exception {
942        try {
943            mDb.close();
944            mDb = null;
945            getProvider().getDatabaseHelper().close();
946        } catch (IllegalStateException e) {
947            e.printStackTrace();
948        }
949        super.tearDown();
950    }
951
952    public void wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db)
953            throws CalendarCache.CacheException {
954        db.beginTransaction();
955
956        // Clean tables
957        db.delete("Calendars", null, null);
958        db.delete("Events", null, null);
959        db.delete("EventsRawTimes", null, null);
960        db.delete("Instances", null, null);
961        db.delete("CalendarMetaData", null, null);
962        db.delete("CalendarCache", null, null);
963        db.delete("Attendees", null, null);
964        db.delete("Reminders", null, null);
965        db.delete("CalendarAlerts", null, null);
966        db.delete("ExtendedProperties", null, null);
967
968        // Set CalendarCache data
969        initCalendarCacheLocked(helper, db);
970
971        // set CalendarMetaData data
972        long now = System.currentTimeMillis();
973        ContentValues values = new ContentValues();
974        values.put("localTimezone", "America/Los_Angeles");
975        values.put("minInstance", 1207008000000L); // 1st April 2008
976        values.put("maxInstance", now + ONE_WEEK_MILLIS);
977        db.insert("CalendarMetaData", null, values);
978
979        db.setTransactionSuccessful();
980        db.endTransaction();
981    }
982
983    private void initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db)
984            throws CalendarCache.CacheException {
985        CalendarCache cache = new CalendarCache(helper);
986
987        String localTimezone = TimeZone.getDefault().getID();
988
989        // Set initial values
990        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_DATABASE_VERSION, "2010k");
991        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_TYPE, CalendarCache.TIMEZONE_TYPE_AUTO);
992        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES, localTimezone);
993        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS, localTimezone);
994    }
995
996    protected CalendarProvider2ForTesting getProvider() {
997        return mProvider;
998    }
999
1000    /**
1001     * Dumps the contents of the given cursor to the log.  For debugging.
1002     * @param cursor the database cursor
1003     */
1004    private void dumpCursor(Cursor cursor) {
1005        cursor.moveToPosition(-1);
1006        String[] cols = cursor.getColumnNames();
1007
1008        Log.i(TAG, "dumpCursor() count: " + cursor.getCount());
1009        int index = 0;
1010        while (cursor.moveToNext()) {
1011            Log.i(TAG, index + " {");
1012            for (int i = 0; i < cols.length; i++) {
1013                Log.i(TAG, "    " + cols[i] + '=' + cursor.getString(i));
1014            }
1015            Log.i(TAG, "}");
1016            index += 1;
1017        }
1018        cursor.moveToPosition(-1);
1019    }
1020
1021    private int insertCal(String name, String timezone) {
1022        return insertCal(name, timezone, DEFAULT_ACCOUNT);
1023    }
1024
1025    /**
1026     * Creates a new calendar, with the provided name, time zone, and account name.
1027     *
1028     * @return the new calendar's _ID value
1029     */
1030    private int insertCal(String name, String timezone, String account) {
1031        ContentValues m = new ContentValues();
1032        m.put(Calendars.NAME, name);
1033        m.put(Calendars.CALENDAR_DISPLAY_NAME, name);
1034        m.put(Calendars.CALENDAR_COLOR, "0xff123456");
1035        m.put(Calendars.CALENDAR_TIME_ZONE, timezone);
1036        m.put(Calendars.VISIBLE, 1);
1037        m.put(Calendars.CAL_SYNC1, CALENDAR_URL);
1038        m.put(Calendars.OWNER_ACCOUNT, account);
1039        m.put(Calendars.ACCOUNT_NAME,  account);
1040        m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1041        m.put(Calendars.SYNC_EVENTS,  1);
1042
1043        Uri url = mResolver.insert(
1044                addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m);
1045        String id = url.getLastPathSegment();
1046        return Integer.parseInt(id);
1047    }
1048
1049    /**
1050     * Constructs a URI from a base URI (e.g. "content://com.android.calendar/calendars"),
1051     * an account name, and an account type.
1052     */
1053    private Uri addSyncQueryParams(Uri uri, String account, String accountType) {
1054        return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
1055                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
1056                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
1057    }
1058
1059    private int deleteMatchingCalendars(String selection, String[] selectionArgs) {
1060        return mResolver.delete(mCalendarsUri, selection, selectionArgs);
1061    }
1062
1063    private Uri insertEvent(int calId, EventInfo event) {
1064        if (mWipe) {
1065            // Wipe instance table so it will be regenerated
1066            mMetaData.clearInstanceRange();
1067        }
1068        ContentValues m = new ContentValues();
1069        m.put(Events.CALENDAR_ID, calId);
1070        m.put(Events.TITLE, event.mTitle);
1071        m.put(Events.DTSTART, event.mDtstart);
1072        m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
1073
1074        if (event.mRrule == null || mForceDtend) {
1075            // This is a normal event
1076            m.put(Events.DTEND, event.mDtend);
1077        }
1078        if (event.mRrule != null) {
1079            // This is a repeating event
1080            m.put(Events.RRULE, event.mRrule);
1081            m.put(Events.DURATION, event.mDuration);
1082        }
1083
1084        if (event.mDescription != null) {
1085            m.put(Events.DESCRIPTION, event.mDescription);
1086        }
1087        if (event.mTimezone != null) {
1088            m.put(Events.EVENT_TIMEZONE, event.mTimezone);
1089        }
1090
1091        if (event.mOriginalTitle != null) {
1092            // This is a recurrence exception.
1093            EventInfo recur = findEvent(event.mOriginalTitle);
1094            assertNotNull(recur);
1095            String syncId = String.format("%d", recur.mSyncId);
1096            m.put(Events.ORIGINAL_SYNC_ID, syncId);
1097            m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0);
1098            m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
1099        }
1100        Uri url = mResolver.insert(mEventsUri, m);
1101
1102        // Create a fake _sync_id and add it to the event.  Update the database
1103        // directly so that we don't trigger any validation checks in the
1104        // CalendarProvider.
1105        long id = ContentUris.parseId(url);
1106        mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id);
1107        event.mSyncId = mGlobalSyncId;
1108        mGlobalSyncId += 1;
1109
1110        return url;
1111    }
1112
1113    /**
1114     * Deletes all the events that match the given title.
1115     * @param title the given title to match events on
1116     * @return the number of rows deleted
1117     */
1118    private int deleteMatchingEvents(String title, String account, String accountType) {
1119        Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID },
1120                "title=?", new String[] { title }, null);
1121        int numRows = 0;
1122        while (cursor.moveToNext()) {
1123            long id = cursor.getLong(0);
1124            // Do delete as a sync adapter so event is really deleted, not just marked
1125            // as deleted.
1126            Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true, account,
1127                    accountType);
1128            numRows += mResolver.delete(uri, null, null);
1129        }
1130        cursor.close();
1131        return numRows;
1132    }
1133
1134    /**
1135     * Updates all the events that match the given title.
1136     * @param title the given title to match events on
1137     * @return the number of rows updated
1138     */
1139    private int updateMatchingEvents(String title, ContentValues values) {
1140        String[] projection = new String[] {
1141                Events._ID,
1142                Events.DTSTART,
1143                Events.DTEND,
1144                Events.DURATION,
1145                Events.ALL_DAY,
1146                Events.RRULE,
1147                Events.EVENT_TIMEZONE,
1148                Events.ORIGINAL_SYNC_ID,
1149        };
1150        Cursor cursor = mResolver.query(mEventsUri, projection,
1151                "title=?", new String[] { title }, null);
1152        int numRows = 0;
1153        while (cursor.moveToNext()) {
1154            long id = cursor.getLong(0);
1155
1156            // If any of the following fields are being changed, then we need
1157            // to include all of them.
1158            if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
1159                    || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
1160                    || values.containsKey(Events.RRULE)
1161                    || values.containsKey(Events.EVENT_TIMEZONE)
1162                    || values.containsKey(Calendar.Events.STATUS)) {
1163                long dtstart = cursor.getLong(1);
1164                long dtend = cursor.getLong(2);
1165                String duration = cursor.getString(3);
1166                boolean allDay = cursor.getInt(4) != 0;
1167                String rrule = cursor.getString(5);
1168                String timezone = cursor.getString(6);
1169                String originalEvent = cursor.getString(7);
1170
1171                if (!values.containsKey(Events.DTSTART)) {
1172                    values.put(Events.DTSTART, dtstart);
1173                }
1174                // Don't add DTEND for repeating events
1175                if (!values.containsKey(Events.DTEND) && rrule == null) {
1176                    values.put(Events.DTEND, dtend);
1177                }
1178                if (!values.containsKey(Events.DURATION) && duration != null) {
1179                    values.put(Events.DURATION, duration);
1180                }
1181                if (!values.containsKey(Events.ALL_DAY)) {
1182                    values.put(Events.ALL_DAY, allDay ? 1 : 0);
1183                }
1184                if (!values.containsKey(Events.RRULE) && rrule != null) {
1185                    values.put(Events.RRULE, rrule);
1186                }
1187                if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
1188                    values.put(Events.EVENT_TIMEZONE, timezone);
1189                }
1190                if (!values.containsKey(Events.ORIGINAL_SYNC_ID) && originalEvent != null) {
1191                    values.put(Events.ORIGINAL_SYNC_ID, originalEvent);
1192                }
1193            }
1194
1195            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1196            numRows += mResolver.update(uri, values, null, null);
1197        }
1198        cursor.close();
1199        return numRows;
1200    }
1201
1202    /**
1203     * Updates the status of all the events that match the given title.
1204     * @param title the given title to match events on
1205     * @return the number of rows updated
1206     */
1207    private int updateMatchingEventsStatusOnly(String title, ContentValues values) {
1208        String[] projection = new String[] {
1209                Events._ID,
1210        };
1211        if (values.size() != 1 && !values.containsKey(Events.STATUS)) {
1212            return 0;
1213        }
1214        Cursor cursor = mResolver.query(mEventsUri, projection,
1215                "title=?", new String[] { title }, null);
1216        int numRows = 0;
1217        while (cursor.moveToNext()) {
1218            long id = cursor.getLong(0);
1219
1220            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1221            numRows += mResolver.update(uri, values, null, null);
1222        }
1223        cursor.close();
1224        return numRows;
1225    }
1226
1227
1228    private void deleteAllEvents() {
1229        mDb.execSQL("DELETE FROM Events;");
1230        mMetaData.clearInstanceRange();
1231    }
1232
1233    public void testInsertNormalEvents() throws Exception {
1234        Cursor cursor;
1235        Uri url = null;
1236
1237        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1238
1239        cursor = mResolver.query(mEventsUri, null, null, null, null);
1240        assertEquals(0, cursor.getCount());
1241        cursor.close();
1242
1243        // Keep track of the number of normal events
1244        int numEvents = 0;
1245
1246        // "begin" is the earliest start time of all the normal events,
1247        // and "end" is the latest end time of all the normal events.
1248        long begin = 0, end = 0;
1249
1250        int len = mEvents.length;
1251        for (int ii = 0; ii < len; ii++) {
1252            EventInfo event = mEvents[ii];
1253            // Skip repeating events and recurrence exceptions
1254            if (event.mRrule != null || event.mOriginalTitle != null) {
1255                continue;
1256            }
1257            if (numEvents == 0) {
1258                begin = event.mDtstart;
1259                end = event.mDtend;
1260            } else {
1261                if (begin > event.mDtstart) {
1262                    begin = event.mDtstart;
1263                }
1264                if (end < event.mDtend) {
1265                    end = event.mDtend;
1266                }
1267            }
1268            url = insertEvent(calId, event);
1269            numEvents += 1;
1270        }
1271
1272        // query one
1273        cursor = mResolver.query(url, null, null, null, null);
1274        assertEquals(1, cursor.getCount());
1275        cursor.close();
1276
1277        // query all
1278        cursor = mResolver.query(mEventsUri, null, null, null, null);
1279        assertEquals(numEvents, cursor.getCount());
1280        cursor.close();
1281
1282        // Check that the Instances table has one instance of each of the
1283        // normal events.
1284        cursor = queryInstances(begin, end);
1285        assertEquals(numEvents, cursor.getCount());
1286        cursor.close();
1287    }
1288
1289    public void testInsertRepeatingEvents() throws Exception {
1290        Cursor cursor;
1291        Uri url = null;
1292
1293        int calId = insertCal("Calendar0", "America/Los_Angeles");
1294
1295        cursor = mResolver.query(mEventsUri, null, null, null, null);
1296        assertEquals(0, cursor.getCount());
1297        cursor.close();
1298
1299        // Keep track of the number of repeating events
1300        int numEvents = 0;
1301
1302        int len = mEvents.length;
1303        for (int ii = 0; ii < len; ii++) {
1304            EventInfo event = mEvents[ii];
1305            // Skip normal events
1306            if (event.mRrule == null) {
1307                continue;
1308            }
1309            url = insertEvent(calId, event);
1310            numEvents += 1;
1311        }
1312
1313        // query one
1314        cursor = mResolver.query(url, null, null, null, null);
1315        assertEquals(1, cursor.getCount());
1316        cursor.close();
1317
1318        // query all
1319        cursor = mResolver.query(mEventsUri, null, null, null, null);
1320        assertEquals(numEvents, cursor.getCount());
1321        cursor.close();
1322    }
1323
1324    // Force a dtend value to be set and make sure instance expansion still works
1325    public void testInstanceRangeDtend() throws Exception {
1326        mForceDtend = true;
1327        testInstanceRange();
1328    }
1329
1330    public void testInstanceRange() throws Exception {
1331        Cursor cursor;
1332        Uri url = null;
1333
1334        int calId = insertCal("Calendar0", "America/Los_Angeles");
1335
1336        cursor = mResolver.query(mEventsUri, null, null, null, null);
1337        assertEquals(0, cursor.getCount());
1338        cursor.close();
1339
1340        int len = mInstanceRanges.length;
1341        for (int ii = 0; ii < len; ii++) {
1342            InstanceInfo instance = mInstanceRanges[ii];
1343            EventInfo event = instance.mEvent;
1344            url = insertEvent(calId, event);
1345            cursor = queryInstances(instance.mBegin, instance.mEnd);
1346            if (instance.mExpectedOccurrences != cursor.getCount()) {
1347                Log.e(TAG, "Test failed! Instance index: " + ii);
1348                Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription
1349                        + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]"
1350                        + " expected: " + instance.mExpectedOccurrences);
1351                dumpCursor(cursor);
1352            }
1353            assertEquals(instance.mExpectedOccurrences, cursor.getCount());
1354            cursor.close();
1355            // Delete as sync_adapter so event is really deleted.
1356            int rows = mResolver.delete(
1357                    updatedUri(url, true, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
1358                    null /* selection */, null /* selection args */);
1359            assertEquals(1, rows);
1360        }
1361    }
1362
1363    public static <T> void assertArrayEquals(T[] expected, T[] actual) {
1364        if (!Arrays.equals(expected, actual)) {
1365            fail("expected:<" + Arrays.toString(expected) +
1366                "> but was:<" + Arrays.toString(actual) + ">");
1367        }
1368    }
1369
1370    @SmallTest @Smoke
1371    public void testEscapeSearchToken() {
1372        String token = "test";
1373        String expected = "test";
1374        assertEquals(expected, mProvider.escapeSearchToken(token));
1375
1376        token = "%";
1377        expected = "#%";
1378        assertEquals(expected, mProvider.escapeSearchToken(token));
1379
1380        token = "_";
1381        expected = "#_";
1382        assertEquals(expected, mProvider.escapeSearchToken(token));
1383
1384        token = "#";
1385        expected = "##";
1386        assertEquals(expected, mProvider.escapeSearchToken(token));
1387
1388        token = "##";
1389        expected = "####";
1390        assertEquals(expected, mProvider.escapeSearchToken(token));
1391
1392        token = "%_#";
1393        expected = "#%#_##";
1394        assertEquals(expected, mProvider.escapeSearchToken(token));
1395
1396        token = "blah%blah";
1397        expected = "blah#%blah";
1398        assertEquals(expected, mProvider.escapeSearchToken(token));
1399    }
1400
1401    @SmallTest @Smoke
1402    public void testTokenizeSearchQuery() {
1403        String query = "";
1404        String[] expectedTokens = new String[] {};
1405        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1406
1407        query = "a";
1408        expectedTokens = new String[] {"a"};
1409        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1410
1411        query = "word";
1412        expectedTokens = new String[] {"word"};
1413        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1414
1415        query = "two words";
1416        expectedTokens = new String[] {"two", "words"};
1417        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1418
1419        query = "test, punctuation.";
1420        expectedTokens = new String[] {"test", "punctuation"};
1421        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1422
1423        query = "\"test phrase\"";
1424        expectedTokens = new String[] {"test phrase"};
1425        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1426
1427        query = "unquoted \"this is quoted\"";
1428        expectedTokens = new String[] {"unquoted", "this is quoted"};
1429        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1430
1431        query = " \"this is quoted\"  unquoted ";
1432        expectedTokens = new String[] {"this is quoted", "unquoted"};
1433        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1434
1435        query = "escap%e m_e";
1436        expectedTokens = new String[] {"escap#%e", "m#_e"};
1437        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1438
1439        query = "'a bunch' of malformed\" things";
1440        expectedTokens = new String[] {"a", "bunch", "of", "malformed", "things"};
1441        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1442
1443        query = "''''''....,.''trim punctuation";
1444        expectedTokens = new String[] {"trim", "punctuation"};
1445        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1446    }
1447
1448    @SmallTest @Smoke
1449    public void testConstructSearchWhere() {
1450        String[] tokens = new String[] {"red"};
1451        String expected = "(title LIKE ? ESCAPE \"#\" OR "
1452            + "description LIKE ? ESCAPE \"#\" OR "
1453            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1454            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1455            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1456        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1457
1458        tokens = new String[] {};
1459        expected = "";
1460        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1461
1462        tokens = new String[] {"red", "green"};
1463        expected = "(title LIKE ? ESCAPE \"#\" OR "
1464                + "description LIKE ? ESCAPE \"#\" OR "
1465                + "eventLocation LIKE ? ESCAPE \"#\" OR "
1466                + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1467                + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1468                + "(title LIKE ? ESCAPE \"#\" OR "
1469                + "description LIKE ? ESCAPE \"#\" OR "
1470                + "eventLocation LIKE ? ESCAPE \"#\" OR "
1471                + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1472                + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1473        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1474
1475        tokens = new String[] {"red blue", "green"};
1476        expected = "(title LIKE ? ESCAPE \"#\" OR "
1477            + "description LIKE ? ESCAPE \"#\" OR "
1478            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1479            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1480            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1481            + "(title LIKE ? ESCAPE \"#\" OR "
1482            + "description LIKE ? ESCAPE \"#\" OR "
1483            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1484            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1485            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1486        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1487    }
1488
1489    @SmallTest @Smoke
1490    public void testConstructSearchArgs() {
1491        long rangeBegin = 0;
1492        long rangeEnd = 10;
1493
1494        String[] tokens = new String[] {"red"};
1495        String[] expected = new String[] {"10", "0", "%red%", "%red%",
1496                "%red%", "%red%", "%red%" };
1497        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1498                rangeBegin, rangeEnd));
1499
1500        tokens = new String[] {"red", "blue"};
1501        expected = new String[] { "10", "0", "%red%", "%red%", "%red%",
1502                "%red%", "%red%", "%blue%", "%blue%",
1503                "%blue%", "%blue%","%blue%"};
1504        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1505                rangeBegin, rangeEnd));
1506
1507        tokens = new String[] {};
1508        expected = new String[] {"10", "0" };
1509        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1510                rangeBegin, rangeEnd));
1511    }
1512
1513    public void testInstanceSearchQuery() throws Exception {
1514        final String[] PROJECTION = new String[] {
1515                Instances.TITLE,                 // 0
1516                Instances.EVENT_LOCATION,        // 1
1517                Instances.ALL_DAY,               // 2
1518                Instances.CALENDAR_COLOR,        // 3
1519                Instances.EVENT_TIMEZONE,        // 4
1520                Instances.EVENT_ID,              // 5
1521                Instances.BEGIN,                 // 6
1522                Instances.END,                   // 7
1523                Instances._ID,                   // 8
1524                Instances.START_DAY,             // 9
1525                Instances.END_DAY,               // 10
1526                Instances.START_MINUTE,          // 11
1527                Instances.END_MINUTE,            // 12
1528                Instances.HAS_ALARM,             // 13
1529                Instances.RRULE,                 // 14
1530                Instances.RDATE,                 // 15
1531                Instances.SELF_ATTENDEE_STATUS,  // 16
1532                Events.ORGANIZER,                // 17
1533                Events.GUESTS_CAN_MODIFY,        // 18
1534        };
1535
1536        String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
1537        String where = Instances.SELF_ATTENDEE_STATUS + "!=" +
1538                Calendar.Attendees.ATTENDEE_STATUS_DECLINED;
1539
1540        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1541        final String START = "2008-05-01T00:00:00";
1542        final String END = "2008-05-01T20:00:00";
1543
1544        EventInfo event1 = new EventInfo("search orange",
1545                START,
1546                END,
1547                false /* allDay */,
1548                DEFAULT_TIMEZONE);
1549        event1.mDescription = "this is description1";
1550
1551        EventInfo event2 = new EventInfo("search purple",
1552                START,
1553                END,
1554                false /* allDay */,
1555                DEFAULT_TIMEZONE);
1556        event2.mDescription = "lasers, out of nowhere";
1557
1558        EventInfo event3 = new EventInfo("",
1559                START,
1560                END,
1561                false /* allDay */,
1562                DEFAULT_TIMEZONE);
1563        event3.mDescription = "kapow";
1564
1565        EventInfo[] events = { event1, event2, event3 };
1566
1567        insertEvent(calId, events[0]);
1568        insertEvent(calId, events[1]);
1569        insertEvent(calId, events[2]);
1570
1571        Time time = new Time(DEFAULT_TIMEZONE);
1572        time.parse3339(START);
1573        long startMs = time.toMillis(true /* ignoreDst */);
1574        // Query starting from way in the past to one hour into the event.
1575        // Query is more than 2 months so the range won't get extended by the provider.
1576        Cursor cursor = null;
1577
1578        try {
1579            cursor = Instances.query(mResolver, PROJECTION,
1580                    startMs - DateUtils.YEAR_IN_MILLIS,
1581                    startMs + DateUtils.HOUR_IN_MILLIS,
1582                    "search", where, null, orderBy);
1583            assertEquals(2, cursor.getCount());
1584        } finally {
1585            if (cursor != null) {
1586                cursor.close();
1587            }
1588        }
1589
1590        try {
1591            cursor = Instances.query(mResolver, PROJECTION,
1592                    startMs - DateUtils.YEAR_IN_MILLIS,
1593                    startMs + DateUtils.HOUR_IN_MILLIS,
1594                    "purple", where, null, orderBy);
1595            assertEquals(1, cursor.getCount());
1596        } finally {
1597            if (cursor != null) {
1598                cursor.close();
1599            }
1600        }
1601
1602        try {
1603            cursor = Instances.query(mResolver, PROJECTION,
1604                    startMs - DateUtils.YEAR_IN_MILLIS,
1605                    startMs + DateUtils.HOUR_IN_MILLIS,
1606                    "puurple", where, null, orderBy);
1607            assertEquals(0, cursor.getCount());
1608        } finally {
1609            if (cursor != null) {
1610                cursor.close();
1611            }
1612        }
1613
1614        try {
1615            cursor = Instances.query(mResolver, PROJECTION,
1616                    startMs - DateUtils.YEAR_IN_MILLIS,
1617                    startMs + DateUtils.HOUR_IN_MILLIS,
1618                    "purple lasers", where, null, orderBy);
1619            assertEquals(1, cursor.getCount());
1620        } finally {
1621            if (cursor != null) {
1622                cursor.close();
1623            }
1624        }
1625
1626        try {
1627            cursor = Instances.query(mResolver, PROJECTION,
1628                    startMs - DateUtils.YEAR_IN_MILLIS,
1629                    startMs + DateUtils.HOUR_IN_MILLIS,
1630                    "lasers kapow", where, null, orderBy);
1631            assertEquals(0, cursor.getCount());
1632        } finally {
1633            if (cursor != null) {
1634                cursor.close();
1635            }
1636        }
1637
1638        try {
1639            cursor = Instances.query(mResolver, PROJECTION,
1640                    startMs - DateUtils.YEAR_IN_MILLIS,
1641                    startMs + DateUtils.HOUR_IN_MILLIS,
1642                    "\"search purple\"", where, null, orderBy);
1643            assertEquals(1, cursor.getCount());
1644        } finally {
1645            if (cursor != null) {
1646                cursor.close();
1647            }
1648        }
1649
1650        try {
1651            cursor = Instances.query(mResolver, PROJECTION,
1652                    startMs - DateUtils.YEAR_IN_MILLIS,
1653                    startMs + DateUtils.HOUR_IN_MILLIS,
1654                    "\"purple search\"", where, null, orderBy);
1655            assertEquals(0, cursor.getCount());
1656        } finally {
1657            if (cursor != null) {
1658                cursor.close();
1659            }
1660        }
1661
1662        try {
1663            cursor = Instances.query(mResolver, PROJECTION,
1664                    startMs - DateUtils.YEAR_IN_MILLIS,
1665                    startMs + DateUtils.HOUR_IN_MILLIS,
1666                    "%", where, null, orderBy);
1667            assertEquals(0, cursor.getCount());
1668        } finally {
1669            if (cursor != null) {
1670                cursor.close();
1671            }
1672        }
1673    }
1674
1675    public void testEntityQuery() throws Exception {
1676        testInsertNormalEvents(); // To initialize
1677
1678        ContentValues reminder = new ContentValues();
1679        reminder.put(Calendar.Reminders.EVENT_ID, 1);
1680        reminder.put(Calendar.Reminders.MINUTES, 10);
1681        reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_SMS);
1682        mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
1683        reminder.put(Calendar.Reminders.MINUTES, 20);
1684        mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
1685
1686        ContentValues extended = new ContentValues();
1687        extended.put(Calendar.ExtendedProperties.NAME, "foo");
1688        extended.put(Calendar.ExtendedProperties.VALUE, "bar");
1689        extended.put(Calendar.ExtendedProperties.EVENT_ID, 2);
1690        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1691        extended.put(Calendar.ExtendedProperties.EVENT_ID, 1);
1692        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1693        extended.put(Calendar.ExtendedProperties.NAME, "foo2");
1694        extended.put(Calendar.ExtendedProperties.VALUE, "bar2");
1695        mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
1696
1697        ContentValues attendee = new ContentValues();
1698        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1699        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
1700        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1701                Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
1702        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1703        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1704                Calendar.Attendees.RELATIONSHIP_PERFORMER);
1705        attendee.put(Calendar.Attendees.EVENT_ID, 3);
1706        mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1707
1708        EntityIterator ei = EventsEntity.newEntityIterator(
1709                mResolver.query(EventsEntity.CONTENT_URI, null, null, null, null), mResolver);
1710        int count = 0;
1711        try {
1712            while (ei.hasNext()) {
1713                Entity entity = ei.next();
1714                ContentValues values = entity.getEntityValues();
1715                assertEquals(CALENDAR_URL, values.getAsString(Calendars.CAL_SYNC1));
1716                ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
1717                switch (values.getAsInteger("_id")) {
1718                    case 1:
1719                        assertEquals(5, subvalues.size()); // 2 x reminder, 3 x extended properties
1720                        break;
1721                    case 2:
1722                        // Extended properties (contains originalTimezone)
1723                        assertEquals(2, subvalues.size());
1724                        ContentValues subContentValues = subvalues.get(1).values;
1725                        String name = subContentValues.getAsString(
1726                                Calendar.ExtendedProperties.NAME);
1727                        String value = subContentValues.getAsString(
1728                                Calendar.ExtendedProperties.VALUE);
1729                        assertEquals("foo", name);
1730                        assertEquals("bar", value);
1731                        break;
1732                    case 3:
1733                        assertEquals(2, subvalues.size()); // Attendees
1734                        break;
1735                    default:
1736                        assertEquals(1, subvalues.size());
1737                        break;
1738                }
1739                count += 1;
1740            }
1741            assertEquals(5, count);
1742        } finally {
1743            ei.close();
1744        }
1745
1746        ei = EventsEntity.newEntityIterator(
1747                    mResolver.query(EventsEntity.CONTENT_URI, null, "_id = 3", null, null),
1748                mResolver);
1749        try {
1750            count = 0;
1751            while (ei.hasNext()) {
1752                Entity entity = ei.next();
1753                count += 1;
1754            }
1755            assertEquals(1, count);
1756        } finally {
1757            ei.close();
1758        }
1759    }
1760
1761    public void testDeleteCalendar() throws Exception {
1762        int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
1763        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1764        insertEvent(calendarId0, mEvents[0]);
1765        insertEvent(calendarId1, mEvents[1]);
1766        // Should have 2 calendars and 2 events
1767        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 2);
1768        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 2);
1769
1770        int deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI,
1771                "ownerAccount='user2@google.com'", null /* selectionArgs */);
1772
1773        assertEquals(1, deletes);
1774        // Should have 1 calendar and 1 event
1775        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 1);
1776        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 1);
1777
1778        deletes = mResolver.delete(Uri.withAppendedPath(Calendar.Calendars.CONTENT_URI,
1779                String.valueOf(calendarId0)),
1780                null /* selection*/ , null /* selectionArgs */);
1781
1782        assertEquals(1, deletes);
1783        // Should have 0 calendars and 0 events
1784        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 0);
1785        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 0);
1786
1787        deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI,
1788                "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */);
1789
1790        assertEquals(0, deletes);
1791    }
1792
1793    public void testCalendarAlerts() throws Exception {
1794        // This projection is from AlertActivity; want to make sure it works.
1795        String[] projection = new String[] {
1796                Calendar.CalendarAlerts._ID,              // 0
1797                Calendar.CalendarAlerts.TITLE,            // 1
1798                Calendar.CalendarAlerts.EVENT_LOCATION,   // 2
1799                Calendar.CalendarAlerts.ALL_DAY,          // 3
1800                Calendar.CalendarAlerts.BEGIN,            // 4
1801                Calendar.CalendarAlerts.END,              // 5
1802                Calendar.CalendarAlerts.EVENT_ID,         // 6
1803                Calendar.CalendarAlerts.CALENDAR_COLOR,   // 7
1804                Calendar.CalendarAlerts.RRULE,            // 8
1805                Calendar.CalendarAlerts.HAS_ALARM,        // 9
1806                Calendar.CalendarAlerts.STATE,            // 10
1807                Calendar.CalendarAlerts.ALARM_TIME,       // 11
1808        };
1809        testInsertNormalEvents(); // To initialize
1810
1811        Uri alertUri = Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1812                2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
1813        Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1814                2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */);
1815
1816        // Regular query
1817        Cursor cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI, projection,
1818                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1819
1820        assertEquals(2, cursor.getCount());
1821        cursor.close();
1822
1823        // Instance query
1824        cursor = mResolver.query(alertUri, projection,
1825                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1826
1827        assertEquals(1, cursor.getCount());
1828        cursor.close();
1829
1830        // Grouped by event query
1831        cursor = mResolver.query(Calendar.CalendarAlerts.CONTENT_URI_BY_INSTANCE, projection,
1832                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1833
1834        assertEquals(1, cursor.getCount());
1835        cursor.close();
1836    }
1837
1838    void checkEvents(int count, SQLiteDatabase db) {
1839        Cursor cursor = db.query("Events", null, null, null, null, null, null);
1840        try {
1841            assertEquals(count, cursor.getCount());
1842        } finally {
1843            cursor.close();
1844        }
1845    }
1846
1847    void checkEvents(int count, SQLiteDatabase db, String calendar) {
1848        Cursor cursor = db.query("Events", null, Events.CALENDAR_ID + "=?", new String[] {calendar},
1849                null, null, null);
1850        try {
1851            assertEquals(count, cursor.getCount());
1852        } finally {
1853            cursor.close();
1854        }
1855    }
1856    /**
1857     * Test attendee processing
1858     * @throws Exception
1859     */
1860    public void testAttendees() throws Exception {
1861        mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE);
1862        String calendarIdString = Integer.toString(mCalendarId);
1863        checkEvents(0, mDb, calendarIdString);
1864        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
1865        // TODO This has a race condition that causes checkEvents to not find
1866        // the just added event
1867        Thread.sleep(200);
1868        checkEvents(1, mDb, calendarIdString);
1869        long eventId = ContentUris.parseId(eventUri);
1870
1871        ContentValues attendee = new ContentValues();
1872        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
1873        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
1874        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
1875        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
1876                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
1877        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
1878        Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1879
1880        Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1881                "event_id=" + eventId, null, null);
1882        assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
1883                cursor.getCount());
1884        cursor.close();
1885
1886        cursor = mResolver.query(eventUri, null, null, null, null);
1887        // TODO figure out why this test fails. App works fine for this case.
1888        assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
1889                cursor.getCount());
1890        int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS);
1891        cursor.moveToNext();
1892        long selfAttendeeStatus = cursor.getInt(selfColumn);
1893        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
1894        cursor.close();
1895
1896        // Change status to declined
1897        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1898                Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
1899        mResolver.update(attendeesUri, attendee, null, null);
1900
1901        cursor = mResolver.query(eventUri, null, null, null, null);
1902        cursor.moveToNext();
1903        selfAttendeeStatus = cursor.getInt(selfColumn);
1904        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1905        cursor.close();
1906
1907        // Add another attendee
1908        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude");
1909        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
1910        attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
1911                Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED);
1912        mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
1913
1914        cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
1915                "event_id=" + mCalendarId, null, null);
1916        assertEquals(2, cursor.getCount());
1917        cursor.close();
1918
1919        cursor = mResolver.query(eventUri, null, null, null, null);
1920        cursor.moveToNext();
1921        selfAttendeeStatus = cursor.getInt(selfColumn);
1922        assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
1923        cursor.close();
1924    }
1925
1926    /**
1927     * Test the event's dirty status and clear it.
1928     *
1929     * @param eventId event to fetch.
1930     * @param wanted the wanted dirty status
1931     */
1932    private void testAndClearDirty(long eventId, int wanted) {
1933        Cursor cursor = mResolver.query(
1934                ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId),
1935                null, null, null, null);
1936        try {
1937            assertEquals("Event count", 1, cursor.getCount());
1938            cursor.moveToNext();
1939            int dirty = cursor.getInt(cursor.getColumnIndex(Calendar.Events.DIRTY));
1940            assertEquals("dirty flag", wanted, dirty);
1941            if (dirty == 1) {
1942                // Have to access database directly since provider will set dirty again.
1943                mDb.execSQL("UPDATE Events SET " + Events.DIRTY + "=0 WHERE _id=" + eventId);
1944            }
1945        } finally {
1946            cursor.close();
1947        }
1948    }
1949
1950    /**
1951     * Test the count of results from a query.
1952     * @param uri The URI to query
1953     * @param where The where string or null.
1954     * @param wanted The number of results wanted.  An assertion is thrown if it doesn't match.
1955     */
1956    private void testQueryCount(Uri uri, String where, int wanted) {
1957        Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */,
1958                null /* sortOrder */);
1959        try {
1960            assertEquals("query results", wanted, cursor.getCount());
1961        } finally {
1962            cursor.close();
1963        }
1964    }
1965
1966    /**
1967     * Test dirty flag processing.
1968     * @throws Exception
1969     */
1970    public void testDirty() throws Exception {
1971        internalTestDirty(false);
1972    }
1973
1974    /**
1975     * Test dirty flag processing for updates from a sync adapter.
1976     * @throws Exception
1977     */
1978    public void testDirtyWithSyncAdapter() throws Exception {
1979        internalTestDirty(true);
1980    }
1981
1982    /**
1983     * Adds CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.  Otherwise,
1984     * returns the original URI.
1985     */
1986    private Uri updatedUri(Uri uri, boolean syncAdapter, String account, String accountType) {
1987        if (syncAdapter) {
1988            return addSyncQueryParams(uri, account, accountType);
1989        } else {
1990            return uri;
1991        }
1992    }
1993
1994    /**
1995     * Test dirty flag processing either for syncAdapter operations or client operations.
1996     * The main difference is syncAdapter operations don't set the dirty bit.
1997     */
1998    private void internalTestDirty(boolean syncAdapter) throws Exception {
1999        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2000
2001        long now = System.currentTimeMillis();
2002        long begin = (now / 1000) * 1000;
2003        long end = begin + ONE_HOUR_MILLIS;
2004        Time time = new Time(DEFAULT_TIMEZONE);
2005        time.set(begin);
2006        String startDate = time.format3339(false);
2007        time.set(end);
2008        String endDate = time.format3339(false);
2009
2010        EventInfo eventInfo = new EventInfo("current", startDate, endDate, false);
2011        Uri eventUri = insertEvent(mCalendarId, eventInfo);
2012
2013        long eventId = ContentUris.parseId(eventUri);
2014        testAndClearDirty(eventId, 1);
2015
2016        ContentValues attendee = new ContentValues();
2017        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
2018        attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
2019        attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
2020        attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
2021                Calendar.Attendees.RELATIONSHIP_ORGANIZER);
2022        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
2023
2024        Uri attendeeUri = mResolver.insert(
2025                updatedUri(Calendar.Attendees.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2026                        DEFAULT_ACCOUNT_TYPE),
2027                attendee);
2028        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2029        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2030
2031        ContentValues reminder = new ContentValues();
2032        reminder.put(Calendar.Reminders.MINUTES, 30);
2033        reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_EMAIL);
2034        reminder.put(Calendar.Attendees.EVENT_ID, eventId);
2035
2036        Uri reminderUri = mResolver.insert(
2037                updatedUri(Calendar.Reminders.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2038                        DEFAULT_ACCOUNT_TYPE), reminder);
2039        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2040        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
2041
2042        long alarmTime = begin + 5 * ONE_MINUTE_MILLIS;
2043
2044        ContentValues alert = new ContentValues();
2045        alert.put(Calendar.CalendarAlerts.BEGIN, begin);
2046        alert.put(Calendar.CalendarAlerts.END, end);
2047        alert.put(Calendar.CalendarAlerts.ALARM_TIME, alarmTime);
2048        alert.put(Calendar.CalendarAlerts.CREATION_TIME, now);
2049        alert.put(Calendar.CalendarAlerts.RECEIVED_TIME, now);
2050        alert.put(Calendar.CalendarAlerts.NOTIFY_TIME, now);
2051        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.SCHEDULED);
2052        alert.put(Calendar.CalendarAlerts.MINUTES, 30);
2053        alert.put(Calendar.CalendarAlerts.EVENT_ID, eventId);
2054
2055        Uri alertUri = mResolver.insert(
2056                updatedUri(Calendar.CalendarAlerts.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2057                        DEFAULT_ACCOUNT_TYPE), alert);
2058        // Alerts don't dirty the event
2059        testAndClearDirty(eventId, 0);
2060        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2061
2062        ContentValues extended = new ContentValues();
2063        extended.put(Calendar.ExtendedProperties.NAME, "foo");
2064        extended.put(Calendar.ExtendedProperties.VALUE, "bar");
2065        extended.put(Calendar.ExtendedProperties.EVENT_ID, eventId);
2066
2067        Uri extendedUri = mResolver.insert(
2068                updatedUri(Calendar.ExtendedProperties.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2069                        DEFAULT_ACCOUNT_TYPE), extended);
2070        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2071        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 2);
2072
2073        // Now test updates
2074
2075        attendee = new ContentValues();
2076        attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Sam");
2077        // Need to include EVENT_ID with attendee update.  Is that desired?
2078        attendee.put(Calendar.Attendees.EVENT_ID, eventId);
2079
2080        assertEquals("update", 1, mResolver.update(
2081                updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2082                attendee,
2083                null /* where */, null /* selectionArgs */));
2084        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2085
2086        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2087
2088        alert = new ContentValues();
2089        alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.DISMISSED);
2090
2091        assertEquals("update", 1, mResolver.update(
2092                updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert,
2093                null /* where */, null /* selectionArgs */));
2094        // Alerts don't dirty the event
2095        testAndClearDirty(eventId, 0);
2096        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2097
2098        extended = new ContentValues();
2099        extended.put(Calendar.ExtendedProperties.VALUE, "baz");
2100
2101        assertEquals("update", 1, mResolver.update(
2102                updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2103                extended,
2104                null /* where */, null /* selectionArgs */));
2105        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2106        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 2);
2107
2108        // Now test deletes
2109
2110        assertEquals("delete", 1, mResolver.delete(
2111                updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2112                null, null /* selectionArgs */));
2113        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2114        testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
2115
2116        assertEquals("delete", 1, mResolver.delete(
2117                updatedUri(reminderUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2118                null /* where */, null /* selectionArgs */));
2119
2120        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2121        testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
2122
2123        assertEquals("delete", 1, mResolver.delete(
2124                updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2125                null /* where */, null /* selectionArgs */));
2126
2127        // Alerts don't dirty the event
2128        testAndClearDirty(eventId, 0);
2129        testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
2130
2131        assertEquals("delete", 1, mResolver.delete(
2132                updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2133                null /* where */, null /* selectionArgs */));
2134
2135        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2136        testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
2137    }
2138
2139    /**
2140     * Test calendar deletion
2141     * @throws Exception
2142     */
2143    public void testCalendarDeletion() throws Exception {
2144        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2145        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
2146        long eventId = ContentUris.parseId(eventUri);
2147        testAndClearDirty(eventId, 1);
2148        Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1"));
2149        long eventId1 = ContentUris.parseId(eventUri);
2150        assertEquals("delete", 1, mResolver.delete(eventUri1, null, null));
2151        // Calendar has one event and one deleted event
2152        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
2153
2154        assertEquals("delete", 1, mResolver.delete(Calendar.Calendars.CONTENT_URI,
2155                "_id=" + mCalendarId, null));
2156        // Calendar should be deleted
2157        testQueryCount(Calendar.Calendars.CONTENT_URI, null, 0);
2158        // Event should be gone
2159        testQueryCount(Calendar.Events.CONTENT_URI, null, 0);
2160    }
2161
2162    /**
2163     * Test multiple account support.
2164     */
2165    public void testMultipleAccounts() throws Exception {
2166        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2167        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
2168        Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0"));
2169        Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"));
2170
2171        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
2172        Uri eventsWithAccount = Calendar.Events.CONTENT_URI.buildUpon()
2173                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_NAME, DEFAULT_ACCOUNT)
2174                .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE)
2175                .build();
2176        // Only one event for that account
2177        testQueryCount(eventsWithAccount, null, 1);
2178
2179        // Test deletion with account and selection
2180
2181        long eventId = ContentUris.parseId(eventUri1);
2182        // Wrong account, should not be deleted
2183        assertEquals("delete", 0, mResolver.delete(
2184                updatedUri(eventsWithAccount, true /* syncAdapter */, DEFAULT_ACCOUNT,
2185                        DEFAULT_ACCOUNT_TYPE),
2186                "_id=" + eventId, null /* selectionArgs */));
2187        testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
2188        // Right account, should be deleted
2189        assertEquals("delete", 1, mResolver.delete(
2190                updatedUri(Calendar.Events.CONTENT_URI, true /* syncAdapter */, "user2@google.com",
2191                        DEFAULT_ACCOUNT_TYPE),
2192                "_id=" + eventId, null /* selectionArgs */));
2193        testQueryCount(Calendar.Events.CONTENT_URI, null, 1);
2194    }
2195
2196    /**
2197     * Run commands, wiping instance table at each step.
2198     * This tests full instance expansion.
2199     * @throws Exception
2200     */
2201    public void testCommandSequences1() throws Exception {
2202        commandSequences(true);
2203    }
2204
2205    /**
2206     * Run commands normally.
2207     * This tests incremental instance expansion.
2208     * @throws Exception
2209     */
2210    public void testCommandSequences2() throws Exception {
2211        commandSequences(false);
2212    }
2213
2214    /**
2215     * Run thorough set of command sequences
2216     * @param wipe true if instances should be wiped and regenerated
2217     * @throws Exception
2218     */
2219    private void commandSequences(boolean wipe) throws Exception {
2220        Cursor cursor;
2221        Uri url = null;
2222        mWipe = wipe; // Set global flag
2223
2224        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2225
2226        cursor = mResolver.query(mEventsUri, null, null, null, null);
2227        assertEquals(0, cursor.getCount());
2228        cursor.close();
2229        Command[] commands;
2230
2231        Log.i(TAG, "Normal insert/delete");
2232        commands = mNormalInsertDelete;
2233        for (Command command : commands) {
2234            command.execute();
2235        }
2236
2237        deleteAllEvents();
2238
2239        Log.i(TAG, "All-day insert/delete");
2240        commands = mAlldayInsertDelete;
2241        for (Command command : commands) {
2242            command.execute();
2243        }
2244
2245        deleteAllEvents();
2246
2247        Log.i(TAG, "Recurring insert/delete");
2248        commands = mRecurringInsertDelete;
2249        for (Command command : commands) {
2250            command.execute();
2251        }
2252
2253        deleteAllEvents();
2254
2255        Log.i(TAG, "Exception with truncated recurrence");
2256        commands = mExceptionWithTruncatedRecurrence;
2257        for (Command command : commands) {
2258            command.execute();
2259        }
2260
2261        deleteAllEvents();
2262
2263        Log.i(TAG, "Exception with moved recurrence");
2264        commands = mExceptionWithMovedRecurrence;
2265        for (Command command : commands) {
2266            command.execute();
2267        }
2268
2269        deleteAllEvents();
2270
2271        Log.i(TAG, "Exception with cancel");
2272        commands = mCancelInstance;
2273        for (Command command : commands) {
2274            command.execute();
2275        }
2276
2277        deleteAllEvents();
2278
2279        Log.i(TAG, "Exception with moved recurrence2");
2280        commands = mExceptionWithMovedRecurrence2;
2281        for (Command command : commands) {
2282            command.execute();
2283        }
2284
2285        deleteAllEvents();
2286
2287        Log.i(TAG, "Exception with no recurrence");
2288        commands = mExceptionWithNoRecurrence;
2289        for (Command command : commands) {
2290            command.execute();
2291        }
2292    }
2293
2294    /**
2295     * Test Time toString.
2296     * @throws Exception
2297     */
2298    // Suppressed because toString currently hangs.
2299    @Suppress
2300    public void testTimeToString() throws Exception {
2301        Time time = new Time(Time.TIMEZONE_UTC);
2302        String str = "2039-01-01T23:00:00.000Z";
2303        String result = "20390101T230000UTC(0,0,0,-1,0)";
2304        time.parse3339(str);
2305        assertEquals(result, time.toString());
2306    }
2307
2308    /**
2309     * Test the query done by Event.loadEvents
2310     * Also test that instance queries work when an event straddles the expansion range
2311     * @throws Exception
2312     */
2313    public void testInstanceQuery() throws Exception {
2314        final String[] PROJECTION = new String[] {
2315                Instances.TITLE,                 // 0
2316                Instances.EVENT_LOCATION,        // 1
2317                Instances.ALL_DAY,               // 2
2318                Instances.CALENDAR_COLOR,        // 3
2319                Instances.EVENT_TIMEZONE,        // 4
2320                Instances.EVENT_ID,              // 5
2321                Instances.BEGIN,                 // 6
2322                Instances.END,                   // 7
2323                Instances._ID,                   // 8
2324                Instances.START_DAY,             // 9
2325                Instances.END_DAY,               // 10
2326                Instances.START_MINUTE,          // 11
2327                Instances.END_MINUTE,            // 12
2328                Instances.HAS_ALARM,             // 13
2329                Instances.RRULE,                 // 14
2330                Instances.RDATE,                 // 15
2331                Instances.SELF_ATTENDEE_STATUS,  // 16
2332                Events.ORGANIZER,                // 17
2333                Events.GUESTS_CAN_MODIFY,        // 18
2334        };
2335
2336        String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
2337        String where = Instances.SELF_ATTENDEE_STATUS + "!=" + Calendar.Attendees.ATTENDEE_STATUS_DECLINED;
2338
2339        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2340        final String START = "2008-05-01T00:00:00";
2341        final String END = "2008-05-01T20:00:00";
2342
2343        EventInfo[] events = { new EventInfo("normal0",
2344                START,
2345                END,
2346                false /* allDay */,
2347                DEFAULT_TIMEZONE) };
2348
2349        insertEvent(calId, events[0]);
2350
2351        Time time = new Time(DEFAULT_TIMEZONE);
2352        time.parse3339(START);
2353        long startMs = time.toMillis(true /* ignoreDst */);
2354        // Query starting from way in the past to one hour into the event.
2355        // Query is more than 2 months so the range won't get extended by the provider.
2356        Cursor cursor = Instances.query(mResolver, PROJECTION,
2357                startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS,
2358                where, null, orderBy);
2359        try {
2360            assertEquals(1, cursor.getCount());
2361        } finally {
2362            cursor.close();
2363        }
2364
2365        // Now expand the instance range.  The event overlaps the new part of the range.
2366        cursor = Instances.query(mResolver, PROJECTION,
2367                startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS,
2368                where, null, orderBy);
2369        try {
2370            assertEquals(1, cursor.getCount());
2371        } finally {
2372            cursor.close();
2373        }
2374    }
2375
2376    private Cursor queryInstances(long begin, long end) {
2377        Uri url = Uri.withAppendedPath(Calendar.Instances.CONTENT_URI, begin + "/" + end);
2378        return mResolver.query(url, null, null, null, null);
2379    }
2380
2381    protected static class MockProvider extends ContentProvider {
2382
2383        private String mAuthority;
2384
2385        private int mNumItems = 0;
2386
2387        public MockProvider(String authority) {
2388            mAuthority = authority;
2389        }
2390
2391        @Override
2392        public boolean onCreate() {
2393            return true;
2394        }
2395
2396        @Override
2397        public Cursor query(Uri uri, String[] projection, String selection,
2398                String[] selectionArgs, String sortOrder) {
2399            return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>());
2400        }
2401
2402        @Override
2403        public String getType(Uri uri) {
2404            throw new UnsupportedOperationException();
2405        }
2406
2407        @Override
2408        public Uri insert(Uri uri, ContentValues values) {
2409            mNumItems++;
2410            return Uri.parse("content://" + mAuthority + "/" + mNumItems);
2411        }
2412
2413        @Override
2414        public int delete(Uri uri, String selection, String[] selectionArgs) {
2415            return 0;
2416        }
2417
2418        @Override
2419        public int update(Uri uri, ContentValues values, String selection,
2420                String[] selectionArgs) {
2421            return 0;
2422        }
2423    }
2424
2425    private void cleanCalendarDataTable(SQLiteOpenHelper helper) {
2426        if (null == helper) {
2427            return;
2428        }
2429        SQLiteDatabase db = helper.getWritableDatabase();
2430        db.execSQL("DELETE FROM CalendarCache;");
2431    }
2432
2433    public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException {
2434        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2435        cleanCalendarDataTable(helper);
2436        CalendarCache cache = new CalendarCache(helper);
2437
2438        boolean hasException = false;
2439        try {
2440            String value = cache.readData(null);
2441        } catch (CalendarCache.CacheException e) {
2442            hasException = true;
2443        }
2444        assertTrue(hasException);
2445
2446        assertNull(cache.readTimezoneDatabaseVersion());
2447
2448        cache.writeTimezoneDatabaseVersion("1234");
2449        assertEquals("1234", cache.readTimezoneDatabaseVersion());
2450
2451        cache.writeTimezoneDatabaseVersion("5678");
2452        assertEquals("5678", cache.readTimezoneDatabaseVersion());
2453    }
2454
2455    private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) {
2456        Uri uri = Uri.parse("content://" + Calendar.AUTHORITY + "/events");
2457        Log.i(TAG, "Looking for EventId = " + eventId);
2458
2459        Cursor cursor = mResolver.query(uri, null, null, null, null);
2460        assertEquals(1, cursor.getCount());
2461
2462        int colIndexTitle = cursor.getColumnIndex(Calendar.Events.TITLE);
2463        int colIndexDtStart = cursor.getColumnIndex(Calendar.Events.DTSTART);
2464        int colIndexDtEnd = cursor.getColumnIndex(Calendar.Events.DTEND);
2465        int colIndexAllDay = cursor.getColumnIndex(Calendar.Events.ALL_DAY);
2466        if (!cursor.moveToNext()) {
2467            Log.e(TAG,"Could not find inserted event");
2468            assertTrue(false);
2469        }
2470        assertEquals(title, cursor.getString(colIndexTitle));
2471        assertEquals(dtStart, cursor.getLong(colIndexDtStart));
2472        assertEquals(dtEnd, cursor.getLong(colIndexDtEnd));
2473        assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0));
2474        cursor.close();
2475    }
2476
2477    public void testChangeTimezoneDB() {
2478        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2479
2480        Cursor cursor = mResolver.query(Calendar.Events.CONTENT_URI, null, null, null, null);
2481        assertEquals(0, cursor.getCount());
2482        cursor.close();
2483
2484        EventInfo[] events = { new EventInfo("normal0",
2485                                        "2008-05-01T00:00:00",
2486                                        "2008-05-02T00:00:00",
2487                                        false,
2488                                        DEFAULT_TIMEZONE) };
2489
2490        Uri uri = insertEvent(calId, events[0]);
2491        assertNotNull(uri);
2492
2493        // check the inserted event
2494        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2495
2496        // inject a new time zone
2497        getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE,
2498                MOCK_TIME_ZONE_DATABASE_VERSION);
2499
2500        // check timezone database version
2501        assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion());
2502
2503        // check that the inserted event has *not* been updated
2504        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2505    }
2506
2507    public static final Uri PROPERTIES_CONTENT_URI =
2508            Uri.parse("content://" + Calendar.AUTHORITY + "/properties");
2509
2510    public static final int COLUMN_KEY_INDEX = 1;
2511    public static final int COLUMN_VALUE_INDEX = 0;
2512
2513    public void testGetProviderProperties() throws CalendarCache.CacheException {
2514        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2515        cleanCalendarDataTable(helper);
2516        CalendarCache cache = new CalendarCache(helper);
2517
2518        cache.writeTimezoneDatabaseVersion("2010k");
2519        cache.writeTimezoneInstances("America/Denver");
2520        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2521        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2522
2523        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, null, null, null);
2524        assertEquals(4, cursor.getCount());
2525
2526        assertEquals(CalendarCache.COLUMN_NAME_KEY, cursor.getColumnName(COLUMN_KEY_INDEX));
2527        assertEquals(CalendarCache.COLUMN_NAME_VALUE, cursor.getColumnName(COLUMN_VALUE_INDEX));
2528
2529        Map<String, String> map = new HashMap<String, String>();
2530
2531        while (cursor.moveToNext()) {
2532            String key = cursor.getString(COLUMN_KEY_INDEX);
2533            String value = cursor.getString(COLUMN_VALUE_INDEX);
2534            map.put(key, value);
2535        }
2536
2537        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2538        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_TYPE));
2539        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES));
2540        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2541
2542        assertEquals("2010k", map.get(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2543        assertEquals("America/Denver", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES));
2544        assertEquals("America/Los_Angeles", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2545        assertEquals(CalendarCache.TIMEZONE_TYPE_AUTO, map.get(CalendarCache.KEY_TIMEZONE_TYPE));
2546
2547        cursor.close();
2548    }
2549
2550    public void testGetProviderPropertiesByKey() throws CalendarCache.CacheException {
2551        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2552        cleanCalendarDataTable(helper);
2553        CalendarCache cache = new CalendarCache(helper);
2554
2555        cache.writeTimezoneDatabaseVersion("2010k");
2556        cache.writeTimezoneInstances("America/Denver");
2557        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2558        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2559
2560        checkValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2561        checkValueForKey("2010k", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2562        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2563        checkValueForKey("America/Los_Angeles", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2564    }
2565
2566    private void checkValueForKey(String value, String key) {
2567        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null,
2568                "key=?", new String[] {key}, null);
2569
2570        assertEquals(1, cursor.getCount());
2571        assertTrue(cursor.moveToFirst());
2572        assertEquals(cursor.getString(COLUMN_KEY_INDEX), key);
2573        assertEquals(cursor.getString(COLUMN_VALUE_INDEX), value);
2574
2575        cursor.close();
2576    }
2577
2578    public void testUpdateProviderProperties() throws CalendarCache.CacheException {
2579        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2580        cleanCalendarDataTable(helper);
2581        CalendarCache cache = new CalendarCache(helper);
2582
2583        String localTimezone = TimeZone.getDefault().getID();
2584
2585        // Set initial value
2586        cache.writeTimezoneDatabaseVersion("2010k");
2587
2588        updateValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2589        checkValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2590
2591        // Set initial values
2592        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2593        cache.writeTimezoneInstances("America/Chicago");
2594        cache.writeTimezoneInstancesPrevious("America/Denver");
2595
2596        updateValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2597        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2598        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2599
2600        updateValueForKey(CalendarCache.TIMEZONE_TYPE_HOME, CalendarCache.KEY_TIMEZONE_TYPE);
2601        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2602        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2603
2604        // Set initial value
2605        cache.writeTimezoneInstancesPrevious("");
2606        updateValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2607        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2608        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2609    }
2610
2611    private void updateValueForKey(String value, String key) {
2612        ContentValues contentValues = new ContentValues();
2613        contentValues.put(CalendarCache.COLUMN_NAME_VALUE, value);
2614
2615        int result = mResolver.update(PROPERTIES_CONTENT_URI,
2616                contentValues,
2617                CalendarCache.COLUMN_NAME_KEY + "=?",
2618                new String[] {key});
2619
2620        assertEquals(1, result);
2621    }
2622
2623    public void testInsertOriginalTimezoneInExtProperties() throws Exception {
2624        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2625
2626
2627        EventInfo[] events = { new EventInfo("normal0",
2628                                        "2008-05-01T00:00:00",
2629                                        "2008-05-02T00:00:00",
2630                                        false,
2631                                        DEFAULT_TIMEZONE) };
2632
2633        Uri eventUri = insertEvent(calId, events[0]);
2634        assertNotNull(eventUri);
2635
2636        long eventId = ContentUris.parseId(eventUri);
2637        assertTrue(eventId > -1);
2638
2639        // check the inserted event
2640        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2641
2642        // Should have 1 calendars and 1 event
2643        testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 1);
2644        testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 1);
2645
2646        // Verify that the original timezone is correct
2647        Cursor cursor = mResolver.query(Calendar.ExtendedProperties.CONTENT_URI,
2648                null/* projection */,
2649                "event_id=" + eventId,
2650                null /* selectionArgs */,
2651                null /* sortOrder */);
2652        try {
2653            // Should have 1 extended property for the original timezone
2654            assertEquals(1, cursor.getCount());
2655
2656            if (cursor.moveToFirst()) {
2657                long id = cursor.getLong(0);
2658                assertEquals(id, eventId);
2659
2660                assertEquals(CalendarProvider2.EXT_PROP_ORIGINAL_TIMEZONE, cursor.getString(2));
2661                assertEquals(DEFAULT_TIMEZONE, cursor.getString(3));
2662            }
2663        } finally {
2664            cursor.close();
2665        }
2666    }
2667
2668    /**
2669     * Verifies that the number of defined calendars meets expectations.
2670     *
2671     * @param expectedCount The number of calendars we expect to find.
2672     */
2673    private void checkCalendarCount(int expectedCount) {
2674        Cursor cursor = mResolver.query(mCalendarsUri,
2675                null /* projection */,
2676                null /* selection */,
2677                null /* selectionArgs */,
2678                null /* sortOrder */);
2679        assertEquals(expectedCount, cursor.getCount());
2680        cursor.close();
2681    }
2682
2683    private void checkCalendarExists(int calId) {
2684        assertTrue(isCalendarExists(calId));
2685    }
2686
2687    private void checkCalendarDoesNotExists(int calId) {
2688        assertFalse(isCalendarExists(calId));
2689    }
2690
2691    private boolean isCalendarExists(int calId) {
2692        Cursor cursor = mResolver.query(mCalendarsUri,
2693                new String[] {Calendars._ID},
2694                null /* selection */,
2695                null /* selectionArgs */,
2696                null /* sortOrder */);
2697        boolean found = false;
2698        while (cursor.moveToNext()) {
2699            if (calId == cursor.getInt(0)) {
2700                found = true;
2701                break;
2702            }
2703        }
2704        cursor.close();
2705        return found;
2706    }
2707
2708    public void testDeleteAllCalendars() {
2709        checkCalendarCount(0);
2710
2711        insertCal("Calendar1", "America/Los_Angeles");
2712        insertCal("Calendar2", "America/Los_Angeles");
2713
2714        checkCalendarCount(2);
2715
2716        deleteMatchingCalendars(null /* selection */, null /* selectionArgs*/);
2717        checkCalendarCount(0);
2718    }
2719
2720    public void testDeleteCalendarsWithSelection() {
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 + "=" + calId2, null /* selectionArgs*/);
2731        checkCalendarCount(1);
2732        checkCalendarExists(calId1);
2733        checkCalendarDoesNotExists(calId2);
2734    }
2735
2736    public void testDeleteCalendarsWithSelectionAndArgs() {
2737        checkCalendarCount(0);
2738
2739        int calId1 = insertCal("Calendar1", "America/Los_Angeles");
2740        int calId2 = insertCal("Calendar2", "America/Los_Angeles");
2741
2742        checkCalendarCount(2);
2743        checkCalendarExists(calId1);
2744        checkCalendarExists(calId2);
2745
2746        deleteMatchingCalendars(Calendars._ID + "=?",
2747                new String[] { Integer.toString(calId2) });
2748        checkCalendarCount(1);
2749        checkCalendarExists(calId1);
2750        checkCalendarDoesNotExists(calId2);
2751
2752        deleteMatchingCalendars(Calendars._ID + "=?" + " AND " + Calendars.NAME + "=?",
2753                new String[] { Integer.toString(calId1), "Calendar1" });
2754        checkCalendarCount(0);
2755    }
2756}
2757