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