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