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