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