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.Intent;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.database.MatrixCursor;
29import android.database.sqlite.SQLiteDatabase;
30import android.database.sqlite.SQLiteOpenHelper;
31import android.net.Uri;
32import android.provider.CalendarContract;
33import android.provider.CalendarContract.Calendars;
34import android.provider.CalendarContract.Colors;
35import android.provider.CalendarContract.Events;
36import android.provider.CalendarContract.Instances;
37import android.test.AndroidTestCase;
38import android.test.IsolatedContext;
39import android.test.RenamingDelegatingContext;
40import android.test.mock.MockContentResolver;
41import android.test.mock.MockContext;
42import android.test.suitebuilder.annotation.SmallTest;
43import android.test.suitebuilder.annotation.Smoke;
44import android.test.suitebuilder.annotation.Suppress;
45import android.text.TextUtils;
46import android.text.format.DateUtils;
47import android.text.format.Time;
48import android.util.Log;
49
50import java.io.File;
51import java.util.Arrays;
52import java.util.HashMap;
53import java.util.Map;
54import java.util.Set;
55import java.util.TimeZone;
56
57/**
58 * Runs various tests on an isolated Calendar provider with its own database.
59 *
60 * You can run the tests with the following command line:
61 *
62 * adb shell am instrument
63 * -e debug false
64 * -w
65 * -e class com.android.providers.calendar.CalendarProvider2Test
66 * com.android.providers.calendar.tests/android.test.InstrumentationTestRunner
67 *
68 * This test no longer extends ProviderTestCase2 because it actually doesn't
69 * allow you to inject a custom context (which we needed to mock out the calls
70 * to start a service). We the next best thing, which is copy the relevant code
71 * from PTC2 and extend AndroidTestCase instead.
72 */
73// flaky test, add back to LargeTest when fixed - bug 2395696
74// @LargeTest
75public class CalendarProvider2Test extends AndroidTestCase {
76    static final String TAG = "calendar";
77
78    private static final String DEFAULT_ACCOUNT_TYPE = "com.google";
79    private static final String DEFAULT_ACCOUNT = "joe@joe.com";
80
81
82    private static final String WHERE_CALENDARS_SELECTED = Calendars.VISIBLE + "=?";
83    private static final String[] WHERE_CALENDARS_ARGS = {
84        "1"
85    };
86    private static final String WHERE_COLOR_ACCOUNT_AND_INDEX = Colors.ACCOUNT_NAME + "=? AND "
87            + Colors.ACCOUNT_TYPE + "=? AND " + Colors.COLOR_KEY + "=?";
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        String mCustomAppPackage;
504        String mCustomAppUri;
505        String mUid2445;
506
507        // Constructor for normal events, using the default timezone
508        public EventInfo(String title, String startDate, String endDate,
509                boolean allDay) {
510            init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE);
511        }
512
513        // Constructor for normal events, specifying the timezone
514        public EventInfo(String title, String startDate, String endDate,
515                boolean allDay, String timezone) {
516            init(title, startDate, endDate, allDay, timezone);
517        }
518
519        public void init(String title, String startDate, String endDate,
520                boolean allDay, String timezone) {
521            mTitle = title;
522            Time time = new Time();
523            if (allDay) {
524                time.timezone = Time.TIMEZONE_UTC;
525            } else if (timezone != null) {
526                time.timezone = timezone;
527            }
528            mTimezone = time.timezone;
529            time.parse3339(startDate);
530            mDtstart = time.toMillis(false /* use isDst */);
531            time.parse3339(endDate);
532            mDtend = time.toMillis(false /* use isDst */);
533            mDuration = null;
534            mRrule = null;
535            mAllDay = allDay;
536            mCustomAppPackage = "CustomAppPackage-" + mTitle;
537            mCustomAppUri = "CustomAppUri-" + mTitle;
538            mUid2445 = null;
539        }
540
541        // Constructor for repeating events, using the default timezone
542        public EventInfo(String title, String description, String startDate, String endDate,
543                String rrule, boolean allDay) {
544            init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE);
545        }
546
547        // Constructor for repeating events, specifying the timezone
548        public EventInfo(String title, String description, String startDate, String endDate,
549                String rrule, boolean allDay, String timezone) {
550            init(title, description, startDate, endDate, rrule, allDay, timezone);
551        }
552
553        public void init(String title, String description, String startDate, String endDate,
554                String rrule, boolean allDay, String timezone) {
555            mTitle = title;
556            mDescription = description;
557            Time time = new Time();
558            if (allDay) {
559                time.timezone = Time.TIMEZONE_UTC;
560            } else if (timezone != null) {
561                time.timezone = timezone;
562            }
563            mTimezone = time.timezone;
564            time.parse3339(startDate);
565            mDtstart = time.toMillis(false /* use isDst */);
566            if (endDate != null) {
567                time.parse3339(endDate);
568                mDtend = time.toMillis(false /* use isDst */);
569            }
570            if (allDay) {
571                long days = 1;
572                if (endDate != null) {
573                    days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS;
574                }
575                mDuration = "P" + days + "D";
576            } else {
577                long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS;
578                mDuration = "P" + seconds + "S";
579            }
580            mRrule = rrule;
581            mAllDay = allDay;
582        }
583
584        // Constructor for recurrence exceptions, using the default timezone
585        public EventInfo(String originalTitle, String originalInstance, String title,
586                String description, String startDate, String endDate, boolean allDay,
587                String customPackageName, String customPackageUri, String mUid2445) {
588            init(originalTitle, originalInstance,
589                    title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE,
590                    customPackageName, customPackageUri, mUid2445);
591        }
592
593        public void init(String originalTitle, String originalInstance,
594                String title, String description, String startDate, String endDate,
595                boolean allDay, String timezone, String customPackageName,
596                String customPackageUri, String uid2445) {
597            mOriginalTitle = originalTitle;
598            Time time = new Time(timezone);
599            time.parse3339(originalInstance);
600            mOriginalInstance = time.toMillis(false /* use isDst */);
601            mCustomAppPackage = customPackageName;
602            mCustomAppUri = customPackageUri;
603            mUid2445 = uid2445;
604            init(title, description, startDate, endDate, null /* rrule */, allDay, timezone);
605        }
606    }
607
608    private class InstanceInfo {
609        EventInfo mEvent;
610        long mBegin;
611        long mEnd;
612        int mExpectedOccurrences;
613
614        public InstanceInfo(String eventName, String startDate, String endDate, int expected) {
615            // Find the test index that contains the given event name
616            mEvent = findEvent(eventName);
617            Time time = new Time(mEvent.mTimezone);
618            time.parse3339(startDate);
619            mBegin = time.toMillis(false /* use isDst */);
620            time.parse3339(endDate);
621            mEnd = time.toMillis(false /* use isDst */);
622            mExpectedOccurrences = expected;
623        }
624    }
625
626    /**
627     * This is the main table of events.  The events in this table are
628     * referred to by name in other places.
629     */
630    private EventInfo[] mEvents = {
631            new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false),
632            new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false),
633            new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false),
634            new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true),
635            new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true),
636            new EventInfo("daily0", "daily from 5/1/2008 12am to 1am",
637                    "2008-05-01T00:00:00", "2008-05-01T01:00:00",
638                    "FREQ=DAILY;WKST=SU", false),
639            new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am",
640                    "2008-05-01T08:30:00", "2008-05-01T09:30:00",
641                    "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false),
642            new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am",
643                    "2008-05-01T08:45:00", "2008-05-01T09:15:00",
644                    "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false),
645            new EventInfo("allday daily0", "all-day daily from 5/1/2008",
646                    "2008-05-01", null,
647                    "FREQ=DAILY;WKST=SU", true),
648            new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008",
649                    "2008-05-01", null,
650                    "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true),
651            new EventInfo("allday weekly0", "all-day weekly from 5/1/2008",
652                    "2008-05-01", null,
653                    "FREQ=WEEKLY;WKST=SU", true),
654            new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008",
655                    "2008-05-01", "2008-05-03",
656                    "FREQ=WEEKLY;WKST=SU", true),
657            new EventInfo("allday yearly0", "all-day yearly on 5/1/2008",
658                    "2008-05-01T", null,
659                    "FREQ=YEARLY;WKST=SU", true),
660            new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm",
661                    "2008-05-06T13:00:00", "2008-05-06T14:00:00",
662                    "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false),
663            new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm",
664                    "2008-05-06T14:30:00", "2008-05-06T15:30:00",
665                    "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false),
666            new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm",
667                    "2008-05-20T15:00:00", "2008-05-20T16:00:00",
668                    "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false),
669            new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am",
670                    "2008-05-01T00:00:00", "2008-05-01T00:10:00",
671                    "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false),
672            new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight",
673                    "2008-05-31T23:00:00", "2008-06-01T00:00:00",
674                    "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false),
675            new EventInfo("daily0", "2008-05-01T00:00:00",
676                    "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am",
677                    "2008-05-01T02:00:00", "2008-05-01T01:03:00", false, "AppPkg1", "AppUri1",
678                    "uid2445-1"),
679            new EventInfo("daily0", "2008-05-03T00:00:00",
680                    "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am",
681                    "2008-05-03T02:00:00", "2008-05-03T01:03:00", false, "AppPkg2", "AppUri2",
682                    null),
683            new EventInfo("daily0", "2008-05-02T00:00:00",
684                    "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008",
685                    "2008-01-02T00:00:00", "2008-01-02T01:00:00", false, "AppPkg3", "AppUri3",
686                    "12345@uid2445"),
687            new EventInfo("weekly0", "2008-05-13T13:00:00",
688                    "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm",
689                    "2008-12-11T13:00:00", "2008-12-11T14:00:00", false, "AppPkg4", "AppUri4",
690                    null),
691            new EventInfo("weekly0", "2008-05-13T13:00:00",
692                    "cancel0", "weekly0 exception for 5/13/2008 1pm",
693                    "2008-05-13T13:00:00", "2008-05-13T14:00:00", false, "AppPkg5", "AppUri5",
694                    null),
695            new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm",
696                    "2008-05-01T13:00:00", "2008-05-01T14:00:00",
697                    "FREQ=YEARLY;WKST=SU", false),
698    };
699
700    /**
701     * This table is used to verify the events generated by mEvents.  It checks that the
702     * number of instances within a given range matches the expected number
703     * of instances.
704     */
705    private InstanceInfo[] mInstanceRanges = {
706            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1),
707            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
708            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2),
709            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2),
710            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
711            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1),
712            new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2),
713            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31),
714            new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32),
715
716            new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
717            new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2),
718
719            new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
720            new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3),
721
722            new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7),
723            new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3),
724            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1),
725            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2),
726            new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5),
727            new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5),
728            new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1),
729            new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2),
730
731            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
732            new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
733            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4),
734            new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8),
735
736            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
737            new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
738            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2),
739            new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4),
740
741            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0),
742            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1),
743            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0),
744            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0),
745            new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1),
746            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
747            new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2),
748
749            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
750            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
751            new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1),
752            new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0),
753            new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2),
754
755            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0),
756            new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1),
757            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1),
758            new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2),
759
760            new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1),
761            new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2),
762    };
763
764    /**
765     * This sequence of commands inserts and deletes some events.
766     */
767    private Command[] mNormalInsertDelete = {
768            new Insert("normal0"),
769            new Insert("normal1"),
770            new Insert("normal2"),
771            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3),
772            new Delete("normal1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
773            new QueryNumEvents(2),
774            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2),
775            new Delete("normal1", 0, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
776            new Delete("normal2", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
777            new QueryNumEvents(1),
778            new Delete("normal0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
779            new QueryNumEvents(0),
780    };
781
782    /**
783     * This sequence of commands inserts and deletes some all-day events.
784     */
785    private Command[] mAlldayInsertDelete = {
786            new Insert("allday0"),
787            new Insert("allday1"),
788            new QueryNumEvents(2),
789            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0),
790            new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2),
791            new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
792            new Delete("allday0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
793            new QueryNumEvents(1),
794            new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
795            new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
796            new Delete("allday1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
797            new QueryNumEvents(0),
798    };
799
800    /**
801     * This sequence of commands inserts and deletes some repeating events.
802     */
803    private Command[] mRecurringInsertDelete = {
804            new Insert("daily0"),
805            new Insert("daily1"),
806            new QueryNumEvents(2),
807            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3),
808            new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2),
809            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6),
810            new Delete("daily1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
811            new QueryNumEvents(1),
812            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2),
813            new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4),
814            new Delete("daily0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
815            new QueryNumEvents(0),
816    };
817
818    /**
819     * This sequence of commands creates a recurring event with a recurrence
820     * exception that moves an event outside the expansion window.  It checks that the
821     * recurrence exception does not occur in the Instances database table.
822     * Bug 1642665
823     */
824    private Command[] mExceptionWithMovedRecurrence = {
825            new Insert("daily0"),
826            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
827                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
828                            "2008-05-03T00:00:00", }),
829            new Insert("except2"),
830            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
831                    new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}),
832    };
833
834    /**
835     * This sequence of commands deletes (cancels) one instance of a recurrence.
836     */
837    private Command[] mCancelInstance = {
838            new Insert("weekly0"),
839            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
840                    new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00",
841                            "2008-05-20T13:00:00", }),
842            new Insert("cancel0"),
843            new Update("cancel0", new KeyValue[] {
844                    new KeyValue(CalendarContract.Events.STATUS,
845                        Integer.toString(CalendarContract.Events.STATUS_CANCELED)),
846            }),
847            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
848                    new String[] {"2008-05-06T13:00:00",
849                            "2008-05-20T13:00:00", }),
850    };
851    /**
852     * This sequence of commands creates a recurring event with a recurrence
853     * exception that moves an event from outside the expansion window into the
854     * expansion window.
855     */
856    private Command[] mExceptionWithMovedRecurrence2 = {
857            new Insert("weekly0"),
858            new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
859                    new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
860                            "2008-12-16T13:00:00", }),
861            new Insert("except3"),
862            new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
863                    new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
864                            "2008-12-11T13:00:00", "2008-12-16T13:00:00", }),
865    };
866    /**
867     * This sequence of commands creates a recurring event with a recurrence
868     * exception and then changes the end time of the recurring event.  It then
869     * checks that the recurrence exception does not occur in the Instances
870     * database table.
871     */
872    private Command[]
873            mExceptionWithTruncatedRecurrence = {
874            new Insert("daily0"),
875            // Verify 4 occurrences of the "daily0" repeating event
876            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
877                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
878                            "2008-05-03T00:00:00", "2008-05-04T00:00:00"}),
879            new Insert("except1"),
880            new QueryNumEvents(2),
881
882            // Verify that one of the 4 occurrences has its start time changed
883            // so that it now matches the recurrence exception.
884            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
885                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
886                            "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
887
888            // Change the end time of "daily0" but it still includes the
889            // recurrence exception.
890            new Update("daily0", new KeyValue[] {
891                    new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"),
892            }),
893
894            // Verify that the recurrence exception is still there
895            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
896                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
897                            "2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
898            // This time change the end time of "daily0" so that it excludes
899            // the recurrence exception.
900            new Update("daily0", new KeyValue[] {
901                    new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"),
902            }),
903            // The server will cancel the out-of-range exception.
904            // It would be nice for the provider to handle this automatically,
905            // but for now simulate the server-side cancel.
906            new Update("except1", new KeyValue[] {
907                new KeyValue(CalendarContract.Events.STATUS,
908                        Integer.toString(CalendarContract.Events.STATUS_CANCELED)),
909            }),
910            // Verify that the recurrence exception does not appear.
911            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
912                    new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}),
913    };
914
915    /**
916     * Bug 135848.  Ensure that a recurrence exception is displayed even if the recurrence
917     * is not present.
918     */
919    private Command[] mExceptionWithNoRecurrence = {
920            new Insert("except0"),
921            new QueryNumEvents(1),
922            new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
923                    new String[] {"2008-05-01T02:00:00"}),
924    };
925
926    private EventInfo findEvent(String name) {
927        int len = mEvents.length;
928        for (int ii = 0; ii < len; ii++) {
929            EventInfo event = mEvents[ii];
930            if (name.equals(event.mTitle)) {
931                return event;
932            }
933        }
934        return null;
935    }
936
937    @Override
938    protected void setUp() throws Exception {
939        super.setUp();
940        // This code here is the code that was originally in ProviderTestCase2
941        mResolver = new MockContentResolver();
942
943        final String filenamePrefix = "test.";
944        RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(
945                new MockContext2(), // The context that most methods are delegated to
946                getContext(), // The context that file methods are delegated to
947                filenamePrefix);
948        mContext = new IsolatedContext(mResolver, targetContextWrapper);
949
950        mProvider = new CalendarProvider2ForTesting();
951        mProvider.attachInfo(mContext, null);
952
953        mResolver.addProvider(CalendarContract.AUTHORITY, mProvider);
954        mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
955        mResolver.addProvider("sync", new MockProvider("sync"));
956
957        mMetaData = getProvider().mMetaData;
958        mForceDtend = false;
959
960        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
961        mDb = helper.getWritableDatabase();
962        wipeAndInitData(helper, mDb);
963    }
964
965    @Override
966    protected void tearDown() throws Exception {
967        try {
968            mDb.close();
969            mDb = null;
970            getProvider().getDatabaseHelper().close();
971        } catch (IllegalStateException e) {
972            e.printStackTrace();
973        }
974        super.tearDown();
975    }
976
977    public void wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db)
978            throws CalendarCache.CacheException {
979        db.beginTransaction();
980
981        // Clean tables
982        db.delete("Calendars", null, null);
983        db.delete("Events", null, null);
984        db.delete("EventsRawTimes", null, null);
985        db.delete("Instances", null, null);
986        db.delete("CalendarMetaData", null, null);
987        db.delete("CalendarCache", null, null);
988        db.delete("Attendees", null, null);
989        db.delete("Reminders", null, null);
990        db.delete("CalendarAlerts", null, null);
991        db.delete("ExtendedProperties", null, null);
992
993        // Set CalendarCache data
994        initCalendarCacheLocked(helper, db);
995
996        // set CalendarMetaData data
997        long now = System.currentTimeMillis();
998        ContentValues values = new ContentValues();
999        values.put("localTimezone", "America/Los_Angeles");
1000        values.put("minInstance", 1207008000000L); // 1st April 2008
1001        values.put("maxInstance", now + ONE_WEEK_MILLIS);
1002        db.insert("CalendarMetaData", null, values);
1003
1004        db.setTransactionSuccessful();
1005        db.endTransaction();
1006    }
1007
1008    private void initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db)
1009            throws CalendarCache.CacheException {
1010        CalendarCache cache = new CalendarCache(helper);
1011
1012        String localTimezone = TimeZone.getDefault().getID();
1013
1014        // Set initial values
1015        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_DATABASE_VERSION, "2010k");
1016        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_TYPE, CalendarCache.TIMEZONE_TYPE_AUTO);
1017        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES, localTimezone);
1018        cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS, localTimezone);
1019    }
1020
1021    protected CalendarProvider2ForTesting getProvider() {
1022        return mProvider;
1023    }
1024
1025    /**
1026     * Dumps the contents of the given cursor to the log.  For debugging.
1027     * @param cursor the database cursor
1028     */
1029    private void dumpCursor(Cursor cursor) {
1030        cursor.moveToPosition(-1);
1031        String[] cols = cursor.getColumnNames();
1032
1033        Log.i(TAG, "dumpCursor() count: " + cursor.getCount());
1034        int index = 0;
1035        while (cursor.moveToNext()) {
1036            Log.i(TAG, index + " {");
1037            for (int i = 0; i < cols.length; i++) {
1038                Log.i(TAG, "    " + cols[i] + '=' + cursor.getString(i));
1039            }
1040            Log.i(TAG, "}");
1041            index += 1;
1042        }
1043        cursor.moveToPosition(-1);
1044    }
1045
1046    private int insertCal(String name, String timezone) {
1047        return insertCal(name, timezone, DEFAULT_ACCOUNT);
1048    }
1049
1050    /**
1051     * Creates a new calendar, with the provided name, time zone, and account name.
1052     *
1053     * @return the new calendar's _ID value
1054     */
1055    private int insertCal(String name, String timezone, String account) {
1056        ContentValues m = new ContentValues();
1057        m.put(Calendars.NAME, name);
1058        m.put(Calendars.CALENDAR_DISPLAY_NAME, name);
1059        m.put(Calendars.CALENDAR_COLOR, 0xff123456);
1060        m.put(Calendars.CALENDAR_TIME_ZONE, timezone);
1061        m.put(Calendars.VISIBLE, 1);
1062        m.put(Calendars.CAL_SYNC1, CALENDAR_URL);
1063        m.put(Calendars.OWNER_ACCOUNT, account);
1064        m.put(Calendars.ACCOUNT_NAME,  account);
1065        m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1066        m.put(Calendars.SYNC_EVENTS,  1);
1067
1068        Uri url = mResolver.insert(
1069                addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m);
1070        String id = url.getLastPathSegment();
1071        return Integer.parseInt(id);
1072    }
1073
1074    private String obsToString(Object... objs) {
1075        StringBuilder bob = new StringBuilder();
1076
1077        for (Object obj : objs) {
1078            bob.append(obj.toString());
1079            bob.append('#');
1080        }
1081
1082        return bob.toString();
1083    }
1084
1085    private Uri insertColor(long colorType, String colorKey, long color) {
1086        ContentValues m = new ContentValues();
1087        m.put(Colors.ACCOUNT_NAME, DEFAULT_ACCOUNT);
1088        m.put(Colors.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
1089        m.put(Colors.DATA, obsToString(colorType, colorKey, color));
1090        m.put(Colors.COLOR_TYPE, colorType);
1091        m.put(Colors.COLOR_KEY, colorKey);
1092        m.put(Colors.COLOR, color);
1093
1094        Uri uri = CalendarContract.Colors.CONTENT_URI;
1095
1096        return mResolver.insert(addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), m);
1097    }
1098
1099    private void updateAndCheckColor(long colorId, long colorType, String colorKey, long color) {
1100
1101        Uri uri = CalendarContract.Colors.CONTENT_URI;
1102
1103        final String where = Colors.ACCOUNT_NAME + "=? AND " + Colors.ACCOUNT_TYPE + "=? AND "
1104                + Colors.COLOR_TYPE + "=? AND " + Colors.COLOR_KEY + "=?";
1105
1106        String[] selectionArgs = new String[] {
1107                DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, Long.toString(colorType), colorKey
1108        };
1109
1110        ContentValues cv = new ContentValues();
1111        cv.put(Colors.COLOR, color);
1112        cv.put(Colors.DATA, obsToString(colorType, colorKey, color));
1113
1114        int count = mResolver.update(
1115                addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), cv, where,
1116                selectionArgs);
1117
1118        checkColor(colorId, colorType, colorKey, color);
1119
1120        assertEquals(1, count);
1121    }
1122
1123    /**
1124     * Constructs a URI from a base URI (e.g. "content://com.android.calendar/calendars"),
1125     * an account name, and an account type.
1126     */
1127    private Uri addSyncQueryParams(Uri uri, String account, String accountType) {
1128        return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
1129                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
1130                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
1131    }
1132
1133    private int deleteMatchingCalendars(String selection, String[] selectionArgs) {
1134        return mResolver.delete(mCalendarsUri, selection, selectionArgs);
1135    }
1136
1137    private Uri insertEvent(int calId, EventInfo event) {
1138        return insertEvent(calId, event, null);
1139    }
1140
1141    private Uri insertEvent(int calId, EventInfo event, ContentValues cv) {
1142        if (mWipe) {
1143            // Wipe instance table so it will be regenerated
1144            mMetaData.clearInstanceRange();
1145        }
1146
1147        if (cv == null) {
1148            cv = eventInfoToContentValues(calId, event);
1149        }
1150
1151        Uri url = mResolver.insert(mEventsUri, cv);
1152
1153        // Create a fake _sync_id and add it to the event.  Update the database
1154        // directly so that we don't trigger any validation checks in the
1155        // CalendarProvider.
1156        long id = ContentUris.parseId(url);
1157        mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id);
1158        event.mSyncId = mGlobalSyncId;
1159        mGlobalSyncId += 1;
1160
1161        return url;
1162    }
1163
1164    private ContentValues eventInfoToContentValues(int calId, EventInfo event) {
1165        ContentValues m = new ContentValues();
1166        m.put(Events.CALENDAR_ID, calId);
1167        m.put(Events.TITLE, event.mTitle);
1168        m.put(Events.DTSTART, event.mDtstart);
1169        m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
1170
1171        if (event.mRrule == null || mForceDtend) {
1172            // This is a normal event
1173            m.put(Events.DTEND, event.mDtend);
1174            m.remove(Events.DURATION);
1175        }
1176        if (event.mRrule != null) {
1177            // This is a repeating event
1178            m.put(Events.RRULE, event.mRrule);
1179            m.put(Events.DURATION, event.mDuration);
1180            m.remove(Events.DTEND);
1181        }
1182
1183        if (event.mDescription != null) {
1184            m.put(Events.DESCRIPTION, event.mDescription);
1185        }
1186        if (event.mTimezone != null) {
1187            m.put(Events.EVENT_TIMEZONE, event.mTimezone);
1188        }
1189        if (event.mCustomAppPackage != null) {
1190            m.put(Events.CUSTOM_APP_PACKAGE, event.mCustomAppPackage);
1191        }
1192        if (event.mCustomAppUri != null) {
1193            m.put(Events.CUSTOM_APP_URI, event.mCustomAppUri);
1194        }
1195        if (event.mUid2445 != null) {
1196            m.put(Events.UID_2445, event.mUid2445);
1197        }
1198
1199        if (event.mOriginalTitle != null) {
1200            // This is a recurrence exception.
1201            EventInfo recur = findEvent(event.mOriginalTitle);
1202            assertNotNull(recur);
1203            String syncId = String.format("%d", recur.mSyncId);
1204            m.put(Events.ORIGINAL_SYNC_ID, syncId);
1205            m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0);
1206            m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
1207        }
1208        return m;
1209    }
1210
1211    /**
1212     * Deletes all the events that match the given title.
1213     * @param title the given title to match events on
1214     * @return the number of rows deleted
1215     */
1216    private int deleteMatchingEvents(String title, String account, String accountType) {
1217        Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID },
1218                "title=?", new String[] { title }, null);
1219        int numRows = 0;
1220        while (cursor.moveToNext()) {
1221            long id = cursor.getLong(0);
1222            // Do delete as a sync adapter so event is really deleted, not just marked
1223            // as deleted.
1224            Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true, account,
1225                    accountType);
1226            numRows += mResolver.delete(uri, null, null);
1227        }
1228        cursor.close();
1229        return numRows;
1230    }
1231
1232    /**
1233     * Updates all the events that match the given title.
1234     * @param title the given title to match events on
1235     * @return the number of rows updated
1236     */
1237    private int updateMatchingEvents(String title, ContentValues values) {
1238        String[] projection = new String[] {
1239                Events._ID,
1240                Events.DTSTART,
1241                Events.DTEND,
1242                Events.DURATION,
1243                Events.ALL_DAY,
1244                Events.RRULE,
1245                Events.EVENT_TIMEZONE,
1246                Events.ORIGINAL_SYNC_ID,
1247        };
1248        Cursor cursor = mResolver.query(mEventsUri, projection,
1249                "title=?", new String[] { title }, null);
1250        int numRows = 0;
1251        while (cursor.moveToNext()) {
1252            long id = cursor.getLong(0);
1253
1254            // If any of the following fields are being changed, then we need
1255            // to include all of them.
1256            if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
1257                    || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
1258                    || values.containsKey(Events.RRULE)
1259                    || values.containsKey(Events.EVENT_TIMEZONE)
1260                    || values.containsKey(CalendarContract.Events.STATUS)) {
1261                long dtstart = cursor.getLong(1);
1262                long dtend = cursor.getLong(2);
1263                String duration = cursor.getString(3);
1264                boolean allDay = cursor.getInt(4) != 0;
1265                String rrule = cursor.getString(5);
1266                String timezone = cursor.getString(6);
1267                String originalEvent = cursor.getString(7);
1268
1269                if (!values.containsKey(Events.DTSTART)) {
1270                    values.put(Events.DTSTART, dtstart);
1271                }
1272                // Don't add DTEND for repeating events
1273                if (!values.containsKey(Events.DTEND) && rrule == null) {
1274                    values.put(Events.DTEND, dtend);
1275                }
1276                if (!values.containsKey(Events.DURATION) && duration != null) {
1277                    values.put(Events.DURATION, duration);
1278                }
1279                if (!values.containsKey(Events.ALL_DAY)) {
1280                    values.put(Events.ALL_DAY, allDay ? 1 : 0);
1281                }
1282                if (!values.containsKey(Events.RRULE) && rrule != null) {
1283                    values.put(Events.RRULE, rrule);
1284                }
1285                if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
1286                    values.put(Events.EVENT_TIMEZONE, timezone);
1287                }
1288                if (!values.containsKey(Events.ORIGINAL_SYNC_ID) && originalEvent != null) {
1289                    values.put(Events.ORIGINAL_SYNC_ID, originalEvent);
1290                }
1291            }
1292
1293            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1294            numRows += mResolver.update(uri, values, null, null);
1295        }
1296        cursor.close();
1297        return numRows;
1298    }
1299
1300    /**
1301     * Updates the status of all the events that match the given title.
1302     * @param title the given title to match events on
1303     * @return the number of rows updated
1304     */
1305    private int updateMatchingEventsStatusOnly(String title, ContentValues values) {
1306        String[] projection = new String[] {
1307                Events._ID,
1308        };
1309        if (values.size() != 1 && !values.containsKey(Events.STATUS)) {
1310            return 0;
1311        }
1312        Cursor cursor = mResolver.query(mEventsUri, projection,
1313                "title=?", new String[] { title }, null);
1314        int numRows = 0;
1315        while (cursor.moveToNext()) {
1316            long id = cursor.getLong(0);
1317
1318            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
1319            numRows += mResolver.update(uri, values, null, null);
1320        }
1321        cursor.close();
1322        return numRows;
1323    }
1324
1325
1326    private void deleteAllEvents() {
1327        mDb.execSQL("DELETE FROM Events;");
1328        mMetaData.clearInstanceRange();
1329    }
1330
1331    /**
1332     * Creates an updated URI that includes query parameters that identify the source as a
1333     * sync adapter.
1334     */
1335    static Uri asSyncAdapter(Uri uri, String account, String accountType) {
1336        return uri.buildUpon()
1337                .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
1338                        "true")
1339                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
1340                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
1341    }
1342
1343    public void testInsertUpdateDeleteColor() throws Exception {
1344        // Calendar Color
1345        long colorType = Colors.TYPE_CALENDAR;
1346        String colorKey = "123";
1347        long colorValue = 11;
1348        long colorId = insertAndCheckColor(colorType, colorKey, colorValue);
1349
1350        try {
1351            insertAndCheckColor(colorType, colorKey, colorValue);
1352            fail("Expected to fail with duplicate insertion");
1353        } catch (IllegalArgumentException iae) {
1354            // good
1355        }
1356
1357        // Test Update
1358        colorValue += 11;
1359        updateAndCheckColor(colorId, colorType, colorKey, colorValue);
1360
1361        // Event Color
1362        colorType = Colors.TYPE_EVENT;
1363        colorValue += 11;
1364        colorId = insertAndCheckColor(colorType, colorKey, colorValue);
1365        try {
1366            insertAndCheckColor(colorType, colorKey, colorValue);
1367            fail("Expected to fail with duplicate insertion");
1368        } catch (IllegalArgumentException iae) {
1369            // good
1370        }
1371
1372        // Create an event with the old color value.
1373        int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
1374        String title = "colorTest";
1375        ContentValues cv = this.eventInfoToContentValues(calendarId0, mEvents[0]);
1376        cv.put(Events.EVENT_COLOR_KEY, colorKey);
1377        cv.put(Events.TITLE, title);
1378        Uri uri = insertEvent(calendarId0, mEvents[0], cv);
1379        Cursor c = mResolver.query(uri, new String[] {Events.EVENT_COLOR},  null, null, null);
1380        try {
1381            // Confirm the color is set.
1382            c.moveToFirst();
1383            assertEquals(colorValue, c.getInt(0));
1384        } finally {
1385            if (c != null) {
1386                c.close();
1387            }
1388        }
1389
1390        // Test Update
1391        colorValue += 11;
1392        updateAndCheckColor(colorId, colorType, colorKey, colorValue);
1393
1394        // Check if color was updated in event.
1395        c = mResolver.query(uri, new String[] {Events.EVENT_COLOR}, null, null, null);
1396        try {
1397            c.moveToFirst();
1398            assertEquals(colorValue, c.getInt(0));
1399        } finally {
1400            if (c != null) {
1401                c.close();
1402            }
1403        }
1404
1405        // Test Delete
1406        Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, DEFAULT_ACCOUNT,
1407                DEFAULT_ACCOUNT_TYPE);
1408        try {
1409            // Delete should fail if color referenced by an event.
1410            mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX,
1411                    new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey});
1412            fail("Should not allow deleting referenced color");
1413        } catch (UnsupportedOperationException e) {
1414            // Exception expected.
1415        }
1416        Cursor cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY},
1417                Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?",
1418                new String[] {colorKey, Long.toString(colorType)}, null);
1419        assertEquals(1, cursor.getCount());
1420
1421        // Try again, by deleting the event, then the color.
1422        assertEquals(1, deleteMatchingEvents(title, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE));
1423        mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX,
1424                new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey});
1425        cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY},
1426                Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?",
1427                new String[] {colorKey, Long.toString(colorType)}, null);
1428        assertEquals(0, cursor.getCount());
1429    }
1430
1431    private void checkColor(long colorId, long colorType, String colorKey, long color) {
1432        String[] projection = new String[] {
1433                Colors.ACCOUNT_NAME, // 0
1434                Colors.ACCOUNT_TYPE, // 1
1435                Colors.COLOR_TYPE,   // 2
1436                Colors.COLOR_KEY,    // 3
1437                Colors.COLOR,        // 4
1438                Colors._ID,          // 5
1439                Colors.DATA,         // 6
1440        };
1441        Cursor cursor = mResolver.query(Colors.CONTENT_URI, projection, Colors.COLOR_KEY
1442                + "=? AND " + Colors.COLOR_TYPE + "=?", new String[] {
1443                colorKey, Long.toString(colorType)
1444        }, null /* sortOrder */);
1445
1446        assertEquals(1, cursor.getCount());
1447
1448        assertTrue(cursor.moveToFirst());
1449        assertEquals(DEFAULT_ACCOUNT, cursor.getString(0));
1450        assertEquals(DEFAULT_ACCOUNT_TYPE, cursor.getString(1));
1451        assertEquals(colorType, cursor.getLong(2));
1452        assertEquals(colorKey, cursor.getString(3));
1453        assertEquals(color, cursor.getLong(4));
1454        assertEquals(colorId, cursor.getLong(5));
1455        assertEquals(obsToString(colorType, colorKey, color), cursor.getString(6));
1456        cursor.close();
1457    }
1458
1459    private long insertAndCheckColor(long colorType, String colorKey, long color) {
1460        Uri uri = insertColor(colorType, colorKey, color);
1461        long id = Long.parseLong(uri.getLastPathSegment());
1462
1463        checkColor(id, colorType, colorKey, color);
1464        return id;
1465    }
1466
1467    public void testInsertNormalEvents() throws Exception {
1468        final int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1469        Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
1470        assertEquals(0, cursor.getCount());
1471        cursor.close();
1472
1473        // Keep track of the number of normal events
1474        int numOfInserts = 0;
1475
1476        // "begin" is the earliest start time of all the normal events,
1477        // and "end" is the latest end time of all the normal events.
1478        long begin = 0, end = 0;
1479
1480        int len = mEvents.length;
1481        Uri[] uris = new Uri[len];
1482        ContentValues[] cvs = new ContentValues[len];
1483        for (int ii = 0; ii < len; ii++) {
1484            EventInfo event = mEvents[ii];
1485            // Skip repeating events and recurrence exceptions
1486            if (event.mRrule != null || event.mOriginalTitle != null) {
1487                continue;
1488            }
1489            if (numOfInserts == 0) {
1490                begin = event.mDtstart;
1491                end = event.mDtend;
1492            } else {
1493                if (begin > event.mDtstart) {
1494                    begin = event.mDtstart;
1495                }
1496                if (end < event.mDtend) {
1497                    end = event.mDtend;
1498                }
1499            }
1500
1501            cvs[ii] = eventInfoToContentValues(calId, event);
1502            uris[ii] = insertEvent(calId, event, cvs[ii]);
1503            numOfInserts += 1;
1504        }
1505
1506        // Verify
1507        for (int i = 0; i < len; i++) {
1508            if (cvs[i] == null) continue;
1509            assertNotNull(uris[i]);
1510            cursor = mResolver.query(uris[i], null, null, null, null);
1511            assertEquals("Item " + i + " not found", 1, cursor.getCount());
1512            verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor);
1513            cursor.close();
1514        }
1515
1516        // query all
1517        cursor = mResolver.query(mEventsUri, null, null, null, null);
1518        assertEquals(numOfInserts, cursor.getCount());
1519        cursor.close();
1520
1521        // Check that the Instances table has one instance of each of the
1522        // normal events.
1523        cursor = queryInstances(begin, end);
1524        assertEquals(numOfInserts, cursor.getCount());
1525        cursor.close();
1526    }
1527
1528    public void testInsertRepeatingEvents() throws Exception {
1529        Cursor cursor;
1530        Uri url = null;
1531
1532        int calId = insertCal("Calendar0", "America/Los_Angeles");
1533
1534        cursor = mResolver.query(mEventsUri, null, null, null, null);
1535        assertEquals(0, cursor.getCount());
1536        cursor.close();
1537
1538        // Keep track of the number of repeating events
1539        int numOfInserts = 0;
1540
1541        int len = mEvents.length;
1542        Uri[] uris = new Uri[len];
1543        ContentValues[] cvs = new ContentValues[len];
1544        for (int ii = 0; ii < len; ii++) {
1545            EventInfo event = mEvents[ii];
1546            // Skip normal events
1547            if (event.mRrule == null) {
1548                continue;
1549            }
1550            cvs[ii] = eventInfoToContentValues(calId, event);
1551            uris[ii] = insertEvent(calId, event, cvs[ii]);
1552            numOfInserts += 1;
1553        }
1554
1555        // Verify
1556        for (int i = 0; i < len; i++) {
1557            if (cvs[i] == null) continue;
1558            assertNotNull(uris[i]);
1559            cursor = mResolver.query(uris[i], null, null, null, null);
1560            assertEquals("Item " + i + " not found", 1, cursor.getCount());
1561            verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor);
1562            cursor.close();
1563        }
1564
1565        // query all
1566        cursor = mResolver.query(mEventsUri, null, null, null, null);
1567        assertEquals(numOfInserts, cursor.getCount());
1568        cursor.close();
1569    }
1570
1571    // Force a dtend value to be set and make sure instance expansion still works
1572    public void testInstanceRangeDtend() throws Exception {
1573        mForceDtend = true;
1574        testInstanceRange();
1575    }
1576
1577    public void testInstanceRange() throws Exception {
1578        Cursor cursor;
1579        Uri url = null;
1580
1581        int calId = insertCal("Calendar0", "America/Los_Angeles");
1582
1583        cursor = mResolver.query(mEventsUri, null, null, null, null);
1584        assertEquals(0, cursor.getCount());
1585        cursor.close();
1586
1587        int len = mInstanceRanges.length;
1588        for (int ii = 0; ii < len; ii++) {
1589            InstanceInfo instance = mInstanceRanges[ii];
1590            EventInfo event = instance.mEvent;
1591            url = insertEvent(calId, event);
1592            cursor = queryInstances(instance.mBegin, instance.mEnd);
1593            if (instance.mExpectedOccurrences != cursor.getCount()) {
1594                Log.e(TAG, "Test failed! Instance index: " + ii);
1595                Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription
1596                        + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]"
1597                        + " expected: " + instance.mExpectedOccurrences);
1598                dumpCursor(cursor);
1599            }
1600            assertEquals(instance.mExpectedOccurrences, cursor.getCount());
1601            cursor.close();
1602            // Delete as sync_adapter so event is really deleted.
1603            int rows = mResolver.delete(
1604                    updatedUri(url, true, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
1605                    null /* selection */, null /* selection args */);
1606            assertEquals(1, rows);
1607        }
1608    }
1609
1610    public static <T> void assertArrayEquals(T[] expected, T[] actual) {
1611        if (!Arrays.equals(expected, actual)) {
1612            fail("expected:<" + Arrays.toString(expected) +
1613                "> but was:<" + Arrays.toString(actual) + ">");
1614        }
1615    }
1616
1617    @SmallTest @Smoke
1618    public void testEscapeSearchToken() {
1619        String token = "test";
1620        String expected = "test";
1621        assertEquals(expected, mProvider.escapeSearchToken(token));
1622
1623        token = "%";
1624        expected = "#%";
1625        assertEquals(expected, mProvider.escapeSearchToken(token));
1626
1627        token = "_";
1628        expected = "#_";
1629        assertEquals(expected, mProvider.escapeSearchToken(token));
1630
1631        token = "#";
1632        expected = "##";
1633        assertEquals(expected, mProvider.escapeSearchToken(token));
1634
1635        token = "##";
1636        expected = "####";
1637        assertEquals(expected, mProvider.escapeSearchToken(token));
1638
1639        token = "%_#";
1640        expected = "#%#_##";
1641        assertEquals(expected, mProvider.escapeSearchToken(token));
1642
1643        token = "blah%blah";
1644        expected = "blah#%blah";
1645        assertEquals(expected, mProvider.escapeSearchToken(token));
1646    }
1647
1648    @SmallTest @Smoke
1649    public void testTokenizeSearchQuery() {
1650        String query = "";
1651        String[] expectedTokens = new String[] {};
1652        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1653
1654        query = "a";
1655        expectedTokens = new String[] {"a"};
1656        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1657
1658        query = "word";
1659        expectedTokens = new String[] {"word"};
1660        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1661
1662        query = "two words";
1663        expectedTokens = new String[] {"two", "words"};
1664        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1665
1666        query = "test, punctuation.";
1667        expectedTokens = new String[] {"test", "punctuation"};
1668        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1669
1670        query = "\"test phrase\"";
1671        expectedTokens = new String[] {"test phrase"};
1672        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1673
1674        query = "unquoted \"this is quoted\"";
1675        expectedTokens = new String[] {"unquoted", "this is quoted"};
1676        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1677
1678        query = " \"this is quoted\"  unquoted ";
1679        expectedTokens = new String[] {"this is quoted", "unquoted"};
1680        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1681
1682        query = "escap%e m_e";
1683        expectedTokens = new String[] {"escap#%e", "m#_e"};
1684        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1685
1686        query = "'a bunch' of malformed\" things";
1687        expectedTokens = new String[] {"a", "bunch", "of", "malformed", "things"};
1688        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1689
1690        query = "''''''....,.''trim punctuation";
1691        expectedTokens = new String[] {"trim", "punctuation"};
1692        assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query));
1693    }
1694
1695    @SmallTest @Smoke
1696    public void testConstructSearchWhere() {
1697        String[] tokens = new String[] {"red"};
1698        String expected = "(title LIKE ? ESCAPE \"#\" OR "
1699            + "description LIKE ? ESCAPE \"#\" OR "
1700            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1701            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1702            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1703        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1704
1705        tokens = new String[] {};
1706        expected = "";
1707        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1708
1709        tokens = new String[] {"red", "green"};
1710        expected = "(title LIKE ? ESCAPE \"#\" OR "
1711                + "description LIKE ? ESCAPE \"#\" OR "
1712                + "eventLocation LIKE ? ESCAPE \"#\" OR "
1713                + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1714                + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1715                + "(title LIKE ? ESCAPE \"#\" OR "
1716                + "description LIKE ? ESCAPE \"#\" OR "
1717                + "eventLocation LIKE ? ESCAPE \"#\" OR "
1718                + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1719                + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1720        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1721
1722        tokens = new String[] {"red blue", "green"};
1723        expected = "(title LIKE ? ESCAPE \"#\" OR "
1724            + "description LIKE ? ESCAPE \"#\" OR "
1725            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1726            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1727            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND "
1728            + "(title LIKE ? ESCAPE \"#\" OR "
1729            + "description LIKE ? ESCAPE \"#\" OR "
1730            + "eventLocation LIKE ? ESCAPE \"#\" OR "
1731            + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR "
1732            + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )";
1733        assertEquals(expected, mProvider.constructSearchWhere(tokens));
1734    }
1735
1736    @SmallTest @Smoke
1737    public void testConstructSearchArgs() {
1738        long rangeBegin = 0;
1739        long rangeEnd = 10;
1740
1741        String[] tokens = new String[] {"red"};
1742        String[] expected = new String[] {"10", "0", "%red%", "%red%",
1743                "%red%", "%red%", "%red%" };
1744        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1745                rangeBegin, rangeEnd));
1746
1747        tokens = new String[] {"red", "blue"};
1748        expected = new String[] { "10", "0", "%red%", "%red%", "%red%",
1749                "%red%", "%red%", "%blue%", "%blue%",
1750                "%blue%", "%blue%","%blue%"};
1751        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1752                rangeBegin, rangeEnd));
1753
1754        tokens = new String[] {};
1755        expected = new String[] {"10", "0" };
1756        assertArrayEquals(expected, mProvider.constructSearchArgs(tokens,
1757                rangeBegin, rangeEnd));
1758    }
1759
1760    public void testInstanceSearchQuery() throws Exception {
1761        final String[] PROJECTION = new String[] {
1762                Instances.TITLE,                 // 0
1763                Instances.EVENT_LOCATION,        // 1
1764                Instances.ALL_DAY,               // 2
1765                Instances.CALENDAR_COLOR,        // 3
1766                Instances.EVENT_TIMEZONE,        // 4
1767                Instances.EVENT_ID,              // 5
1768                Instances.BEGIN,                 // 6
1769                Instances.END,                   // 7
1770                Instances._ID,                   // 8
1771                Instances.START_DAY,             // 9
1772                Instances.END_DAY,               // 10
1773                Instances.START_MINUTE,          // 11
1774                Instances.END_MINUTE,            // 12
1775                Instances.HAS_ALARM,             // 13
1776                Instances.RRULE,                 // 14
1777                Instances.RDATE,                 // 15
1778                Instances.SELF_ATTENDEE_STATUS,  // 16
1779                Events.ORGANIZER,                // 17
1780                Events.GUESTS_CAN_MODIFY,        // 18
1781        };
1782
1783        String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
1784        String where = Instances.SELF_ATTENDEE_STATUS + "!=" +
1785                CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED;
1786
1787        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
1788        final String START = "2008-05-01T00:00:00";
1789        final String END = "2008-05-01T20:00:00";
1790
1791        EventInfo event1 = new EventInfo("search orange",
1792                START,
1793                END,
1794                false /* allDay */,
1795                DEFAULT_TIMEZONE);
1796        event1.mDescription = "this is description1";
1797
1798        EventInfo event2 = new EventInfo("search purple",
1799                START,
1800                END,
1801                false /* allDay */,
1802                DEFAULT_TIMEZONE);
1803        event2.mDescription = "lasers, out of nowhere";
1804
1805        EventInfo event3 = new EventInfo("",
1806                START,
1807                END,
1808                false /* allDay */,
1809                DEFAULT_TIMEZONE);
1810        event3.mDescription = "kapow";
1811
1812        EventInfo[] events = { event1, event2, event3 };
1813
1814        insertEvent(calId, events[0]);
1815        insertEvent(calId, events[1]);
1816        insertEvent(calId, events[2]);
1817
1818        Time time = new Time(DEFAULT_TIMEZONE);
1819        time.parse3339(START);
1820        long startMs = time.toMillis(true /* ignoreDst */);
1821        // Query starting from way in the past to one hour into the event.
1822        // Query is more than 2 months so the range won't get extended by the provider.
1823        Cursor cursor = null;
1824
1825        try {
1826            cursor = queryInstances(mResolver, PROJECTION,
1827                    startMs - DateUtils.YEAR_IN_MILLIS,
1828                    startMs + DateUtils.HOUR_IN_MILLIS,
1829                    "search", where, null, orderBy);
1830            assertEquals(2, cursor.getCount());
1831        } finally {
1832            if (cursor != null) {
1833                cursor.close();
1834            }
1835        }
1836
1837        try {
1838            cursor = queryInstances(mResolver, PROJECTION,
1839                    startMs - DateUtils.YEAR_IN_MILLIS,
1840                    startMs + DateUtils.HOUR_IN_MILLIS,
1841                    "purple", where, null, orderBy);
1842            assertEquals(1, cursor.getCount());
1843        } finally {
1844            if (cursor != null) {
1845                cursor.close();
1846            }
1847        }
1848
1849        try {
1850            cursor = queryInstances(mResolver, PROJECTION,
1851                    startMs - DateUtils.YEAR_IN_MILLIS,
1852                    startMs + DateUtils.HOUR_IN_MILLIS,
1853                    "puurple", where, null, orderBy);
1854            assertEquals(0, cursor.getCount());
1855        } finally {
1856            if (cursor != null) {
1857                cursor.close();
1858            }
1859        }
1860
1861        try {
1862            cursor = queryInstances(mResolver, PROJECTION,
1863                    startMs - DateUtils.YEAR_IN_MILLIS,
1864                    startMs + DateUtils.HOUR_IN_MILLIS,
1865                    "purple lasers", where, null, orderBy);
1866            assertEquals(1, cursor.getCount());
1867        } finally {
1868            if (cursor != null) {
1869                cursor.close();
1870            }
1871        }
1872
1873        try {
1874            cursor = queryInstances(mResolver, PROJECTION,
1875                    startMs - DateUtils.YEAR_IN_MILLIS,
1876                    startMs + DateUtils.HOUR_IN_MILLIS,
1877                    "lasers kapow", where, null, orderBy);
1878            assertEquals(0, cursor.getCount());
1879        } finally {
1880            if (cursor != null) {
1881                cursor.close();
1882            }
1883        }
1884
1885        try {
1886            cursor = queryInstances(mResolver, PROJECTION,
1887                    startMs - DateUtils.YEAR_IN_MILLIS,
1888                    startMs + DateUtils.HOUR_IN_MILLIS,
1889                    "\"search purple\"", where, null, orderBy);
1890            assertEquals(1, cursor.getCount());
1891        } finally {
1892            if (cursor != null) {
1893                cursor.close();
1894            }
1895        }
1896
1897        try {
1898            cursor = queryInstances(mResolver, PROJECTION,
1899                    startMs - DateUtils.YEAR_IN_MILLIS,
1900                    startMs + DateUtils.HOUR_IN_MILLIS,
1901                    "\"purple search\"", where, null, orderBy);
1902            assertEquals(0, cursor.getCount());
1903        } finally {
1904            if (cursor != null) {
1905                cursor.close();
1906            }
1907        }
1908
1909        try {
1910            cursor = queryInstances(mResolver, PROJECTION,
1911                    startMs - DateUtils.YEAR_IN_MILLIS,
1912                    startMs + DateUtils.HOUR_IN_MILLIS,
1913                    "%", where, null, orderBy);
1914            assertEquals(0, cursor.getCount());
1915        } finally {
1916            if (cursor != null) {
1917                cursor.close();
1918            }
1919        }
1920    }
1921
1922    public void testDeleteCalendar() throws Exception {
1923        int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE);
1924        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
1925        insertEvent(calendarId0, mEvents[0]);
1926        insertEvent(calendarId1, mEvents[1]);
1927        // Should have 2 calendars and 2 events
1928        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 2);
1929        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 2);
1930
1931        int deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
1932                "ownerAccount='user2@google.com'", null /* selectionArgs */);
1933
1934        assertEquals(1, deletes);
1935        // Should have 1 calendar and 1 event
1936        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 1);
1937        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 1);
1938
1939        deletes = mResolver.delete(Uri.withAppendedPath(CalendarContract.Calendars.CONTENT_URI,
1940                String.valueOf(calendarId0)),
1941                null /* selection*/ , null /* selectionArgs */);
1942
1943        assertEquals(1, deletes);
1944        // Should have 0 calendars and 0 events
1945        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 0);
1946        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 0);
1947
1948        deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
1949                "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */);
1950
1951        assertEquals(0, deletes);
1952    }
1953
1954    public void testCalendarAlerts() throws Exception {
1955        // This projection is from AlertActivity; want to make sure it works.
1956        String[] projection = new String[] {
1957                CalendarContract.CalendarAlerts._ID,              // 0
1958                CalendarContract.CalendarAlerts.TITLE,            // 1
1959                CalendarContract.CalendarAlerts.EVENT_LOCATION,   // 2
1960                CalendarContract.CalendarAlerts.ALL_DAY,          // 3
1961                CalendarContract.CalendarAlerts.BEGIN,            // 4
1962                CalendarContract.CalendarAlerts.END,              // 5
1963                CalendarContract.CalendarAlerts.EVENT_ID,         // 6
1964                CalendarContract.CalendarAlerts.CALENDAR_COLOR,   // 7
1965                CalendarContract.CalendarAlerts.RRULE,            // 8
1966                CalendarContract.CalendarAlerts.HAS_ALARM,        // 9
1967                CalendarContract.CalendarAlerts.STATE,            // 10
1968                CalendarContract.CalendarAlerts.ALARM_TIME,       // 11
1969        };
1970        testInsertNormalEvents(); // To initialize
1971
1972        Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1973                2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
1974        CalendarContract.CalendarAlerts.insert(mResolver, 1 /* eventId */,
1975                2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */);
1976
1977        // Regular query
1978        Cursor cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI, projection,
1979                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1980
1981        assertEquals(2, cursor.getCount());
1982        cursor.close();
1983
1984        // Instance query
1985        cursor = mResolver.query(alertUri, projection,
1986                null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1987
1988        assertEquals(1, cursor.getCount());
1989        cursor.close();
1990
1991        // Grouped by event query
1992        cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI_BY_INSTANCE,
1993                projection, null /* selection */, null /* selectionArgs */, null /* sortOrder */);
1994
1995        assertEquals(1, cursor.getCount());
1996        cursor.close();
1997    }
1998
1999    void checkEvents(int count, SQLiteDatabase db) {
2000        Cursor cursor = db.query("Events", null, null, null, null, null, null);
2001        try {
2002            assertEquals(count, cursor.getCount());
2003        } finally {
2004            cursor.close();
2005        }
2006    }
2007
2008    void checkEvents(int count, SQLiteDatabase db, String calendar) {
2009        Cursor cursor = db.query("Events", null, Events.CALENDAR_ID + "=?", new String[] {calendar},
2010                null, null, null);
2011        try {
2012            assertEquals(count, cursor.getCount());
2013        } finally {
2014            cursor.close();
2015        }
2016    }
2017
2018
2019//    TODO Reenable this when we are ready to work on this
2020//
2021//    public void testToShowInsertIsSlowForRecurringEvents() throws Exception {
2022//        mCalendarId = insertCal("CalendarTestToShowInsertIsSlowForRecurringEvents", DEFAULT_TIMEZONE);
2023//        String calendarIdString = Integer.toString(mCalendarId);
2024//        long testStart = System.currentTimeMillis();
2025//
2026//        final int testTrials = 100;
2027//
2028//        for (int i = 0; i < testTrials; i++) {
2029//            checkEvents(i, mDb, calendarIdString);
2030//            long insertStartTime = System.currentTimeMillis();
2031//            Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
2032//            Log.e(TAG, i + ") insertion time " + (System.currentTimeMillis() - insertStartTime));
2033//        }
2034//        Log.e(TAG, " Avg insertion time = " + (System.currentTimeMillis() - testStart)/testTrials);
2035//    }
2036
2037    /**
2038     * Test attendee processing
2039     * @throws Exception
2040     */
2041    public void testAttendees() throws Exception {
2042        mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE);
2043        String calendarIdString = Integer.toString(mCalendarId);
2044        checkEvents(0, mDb, calendarIdString);
2045        Uri eventUri = insertEvent(mCalendarId, findEvent("normal0"));
2046        checkEvents(1, mDb, calendarIdString);
2047        long eventId = ContentUris.parseId(eventUri);
2048
2049        ContentValues attendee = new ContentValues();
2050        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe");
2051        attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
2052        attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE,
2053                CalendarContract.Attendees.TYPE_REQUIRED);
2054        attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
2055                CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
2056        attendee.put(CalendarContract.Attendees.EVENT_ID, eventId);
2057        attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID1");
2058        attendee.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, "IDNS1");
2059        Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee);
2060
2061        Cursor cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null,
2062                "event_id=" + eventId, null, null);
2063        assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
2064                cursor.getCount());
2065        Set<String> attendeeColumns = attendee.keySet();
2066        verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor);
2067        cursor.close();
2068
2069        cursor = mResolver.query(eventUri, null, null, null, null);
2070        // TODO figure out why this test fails. App works fine for this case.
2071        assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1,
2072                cursor.getCount());
2073        int selfColumn = cursor.getColumnIndex(CalendarContract.Events.SELF_ATTENDEE_STATUS);
2074        cursor.moveToNext();
2075        long selfAttendeeStatus = cursor.getInt(selfColumn);
2076        assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
2077        cursor.close();
2078
2079        // Update status to declined and change identity
2080        ContentValues attendeeUpdate = new ContentValues();
2081        attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2");
2082        attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2");
2083        attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_STATUS,
2084                CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED);
2085        attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS,
2086                CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED);
2087        mResolver.update(attendeesUri, attendeeUpdate, null, null);
2088
2089        // Check in attendees table
2090        cursor = mResolver.query(attendeesUri, null, null, null, null);
2091        cursor.moveToNext();
2092        verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor);
2093        cursor.close();
2094
2095        // Test that the self status in events table is updated
2096        cursor = mResolver.query(eventUri, null, null, null, null);
2097        cursor.moveToNext();
2098        selfAttendeeStatus = cursor.getInt(selfColumn);
2099        assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
2100        cursor.close();
2101
2102        // Add another attendee
2103        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Dude");
2104        attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
2105        attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS,
2106                CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED);
2107        mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee);
2108
2109        cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null,
2110                "event_id=" + eventId, null, null);
2111        assertEquals(2, cursor.getCount());
2112        cursor.close();
2113
2114        cursor = mResolver.query(eventUri, null, null, null, null);
2115        cursor.moveToNext();
2116        selfAttendeeStatus = cursor.getInt(selfColumn);
2117        assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
2118        cursor.close();
2119    }
2120
2121    private void verifyContentValueAgainstCursor(ContentValues cv,
2122            Set<String> keys, Cursor cursor) {
2123        cursor.moveToFirst();
2124        for (String key : keys) {
2125            assertEquals(cv.get(key).toString(),
2126                    cursor.getString(cursor.getColumnIndex(key)));
2127        }
2128        cursor.close();
2129    }
2130
2131    /**
2132     * Test the event's dirty status and clear it.
2133     *
2134     * @param eventId event to fetch.
2135     * @param wanted the wanted dirty status
2136     */
2137    private void testAndClearDirty(long eventId, int wanted) {
2138        Cursor cursor = mResolver.query(
2139                ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId),
2140                null, null, null, null);
2141        try {
2142            assertEquals("Event count", 1, cursor.getCount());
2143            cursor.moveToNext();
2144            int dirty = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.DIRTY));
2145            assertEquals("dirty flag", wanted, dirty);
2146            if (dirty == 1) {
2147                // Have to access database directly since provider will set dirty again.
2148                mDb.execSQL("UPDATE Events SET " + Events.DIRTY + "=0 WHERE _id=" + eventId);
2149            }
2150        } finally {
2151            cursor.close();
2152        }
2153    }
2154
2155    /**
2156     * Test the count of results from a query.
2157     * @param uri The URI to query
2158     * @param where The where string or null.
2159     * @param wanted The number of results wanted.  An assertion is thrown if it doesn't match.
2160     */
2161    private void testQueryCount(Uri uri, String where, int wanted) {
2162        Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */,
2163                null /* sortOrder */);
2164        try {
2165            assertEquals("query results", wanted, cursor.getCount());
2166        } finally {
2167            cursor.close();
2168        }
2169    }
2170
2171    /**
2172     * Test dirty flag processing.
2173     * @throws Exception
2174     */
2175    public void testDirty() throws Exception {
2176        internalTestDirty(false);
2177    }
2178
2179    /**
2180     * Test dirty flag processing for updates from a sync adapter.
2181     * @throws Exception
2182     */
2183    public void testDirtyWithSyncAdapter() throws Exception {
2184        internalTestDirty(true);
2185    }
2186
2187    /**
2188     * Adds CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.  Otherwise,
2189     * returns the original URI.
2190     */
2191    private Uri updatedUri(Uri uri, boolean syncAdapter, String account, String accountType) {
2192        if (syncAdapter) {
2193            return addSyncQueryParams(uri, account, accountType);
2194        } else {
2195            return uri;
2196        }
2197    }
2198
2199    /**
2200     * Test dirty flag processing either for syncAdapter operations or client operations.
2201     * The main difference is syncAdapter operations don't set the dirty bit.
2202     */
2203    private void internalTestDirty(boolean syncAdapter) throws Exception {
2204        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2205
2206        long now = System.currentTimeMillis();
2207        long begin = (now / 1000) * 1000;
2208        long end = begin + ONE_HOUR_MILLIS;
2209        Time time = new Time(DEFAULT_TIMEZONE);
2210        time.set(begin);
2211        String startDate = time.format3339(false);
2212        time.set(end);
2213        String endDate = time.format3339(false);
2214
2215        EventInfo eventInfo = new EventInfo("current", startDate, endDate, false);
2216        Uri eventUri = insertEvent(mCalendarId, eventInfo);
2217
2218        long eventId = ContentUris.parseId(eventUri);
2219        testAndClearDirty(eventId, 1);
2220
2221        ContentValues attendee = new ContentValues();
2222        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe");
2223        attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT);
2224        attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE,
2225                CalendarContract.Attendees.TYPE_REQUIRED);
2226        attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
2227                CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
2228        attendee.put(CalendarContract.Attendees.EVENT_ID, eventId);
2229
2230        Uri attendeeUri = mResolver.insert(
2231                updatedUri(CalendarContract.Attendees.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2232                        DEFAULT_ACCOUNT_TYPE),
2233                attendee);
2234        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2235        testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2236
2237        ContentValues reminder = new ContentValues();
2238        reminder.put(CalendarContract.Reminders.MINUTES, 30);
2239        reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL);
2240        reminder.put(CalendarContract.Attendees.EVENT_ID, eventId);
2241
2242        Uri reminderUri = mResolver.insert(
2243                updatedUri(CalendarContract.Reminders.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT,
2244                        DEFAULT_ACCOUNT_TYPE), reminder);
2245        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2246        testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
2247
2248        long alarmTime = begin + 5 * ONE_MINUTE_MILLIS;
2249
2250        ContentValues alert = new ContentValues();
2251        alert.put(CalendarContract.CalendarAlerts.BEGIN, begin);
2252        alert.put(CalendarContract.CalendarAlerts.END, end);
2253        alert.put(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime);
2254        alert.put(CalendarContract.CalendarAlerts.CREATION_TIME, now);
2255        alert.put(CalendarContract.CalendarAlerts.RECEIVED_TIME, now);
2256        alert.put(CalendarContract.CalendarAlerts.NOTIFY_TIME, now);
2257        alert.put(CalendarContract.CalendarAlerts.STATE,
2258                CalendarContract.CalendarAlerts.STATE_SCHEDULED);
2259        alert.put(CalendarContract.CalendarAlerts.MINUTES, 30);
2260        alert.put(CalendarContract.CalendarAlerts.EVENT_ID, eventId);
2261
2262        Uri alertUri = mResolver.insert(
2263                updatedUri(CalendarContract.CalendarAlerts.CONTENT_URI, syncAdapter,
2264                        DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert);
2265        // Alerts don't dirty the event
2266        testAndClearDirty(eventId, 0);
2267        testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2268
2269        ContentValues extended = new ContentValues();
2270        extended.put(CalendarContract.ExtendedProperties.NAME, "foo");
2271        extended.put(CalendarContract.ExtendedProperties.VALUE, "bar");
2272        extended.put(CalendarContract.ExtendedProperties.EVENT_ID, eventId);
2273
2274        Uri extendedUri = null;
2275        if (syncAdapter) {
2276            // Only the sync adapter is allowed to modify ExtendedProperties.
2277            extendedUri = mResolver.insert(
2278                    updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter,
2279                            DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended);
2280            testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2281            testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI,
2282                    "event_id=" + eventId, 2);
2283        } else {
2284            // Confirm that inserting as app fails.
2285            try {
2286                extendedUri = mResolver.insert(
2287                        updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter,
2288                                DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended);
2289                fail("Only sync adapter should be allowed to insert into ExtendedProperties");
2290            } catch (IllegalArgumentException iae) {}
2291        }
2292
2293        // Now test updates
2294
2295        attendee = new ContentValues();
2296        attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Sam");
2297
2298        assertEquals("update", 1, mResolver.update(
2299                updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2300                attendee,
2301                null /* where */, null /* selectionArgs */));
2302        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2303
2304        testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
2305
2306        alert = new ContentValues();
2307        alert.put(CalendarContract.CalendarAlerts.STATE,
2308                CalendarContract.CalendarAlerts.STATE_DISMISSED);
2309
2310        assertEquals("update", 1, mResolver.update(
2311                updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert,
2312                null /* where */, null /* selectionArgs */));
2313        // Alerts don't dirty the event
2314        testAndClearDirty(eventId, 0);
2315        testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
2316
2317        extended = new ContentValues();
2318        extended.put(CalendarContract.ExtendedProperties.VALUE, "baz");
2319
2320        if (syncAdapter) {
2321            assertEquals("update", 1, mResolver.update(
2322                    updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2323                    extended,
2324                    null /* where */, null /* selectionArgs */));
2325            testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2326            testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI,
2327                    "event_id=" + eventId, 2);
2328        }
2329
2330        // Now test deletes
2331
2332        assertEquals("delete", 1, mResolver.delete(
2333                updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2334                null, null /* selectionArgs */));
2335        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2336        testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
2337
2338        assertEquals("delete", 1, mResolver.delete(
2339                updatedUri(reminderUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2340                null /* where */, null /* selectionArgs */));
2341
2342        testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2343        testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
2344
2345        assertEquals("delete", 1, mResolver.delete(
2346                updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2347                null /* where */, null /* selectionArgs */));
2348
2349        // Alerts don't dirty the event
2350        testAndClearDirty(eventId, 0);
2351        testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
2352
2353        if (syncAdapter) {
2354            assertEquals("delete", 1, mResolver.delete(
2355                    updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE),
2356                    null /* where */, null /* selectionArgs */));
2357
2358            testAndClearDirty(eventId, syncAdapter ? 0 : 1);
2359            testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
2360        }
2361    }
2362
2363    /**
2364     * Test calendar deletion
2365     * @throws Exception
2366     */
2367    public void testCalendarDeletion() throws Exception {
2368        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2369        Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
2370        long eventId = ContentUris.parseId(eventUri);
2371        testAndClearDirty(eventId, 1);
2372        Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1"));
2373        long eventId1 = ContentUris.parseId(eventUri);
2374        assertEquals("delete", 1, mResolver.delete(eventUri1, null, null));
2375        // Calendar has one event and one deleted event
2376        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2377
2378        assertEquals("delete", 1, mResolver.delete(CalendarContract.Calendars.CONTENT_URI,
2379                "_id=" + mCalendarId, null));
2380        // Calendar should be deleted
2381        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null, 0);
2382        // Event should be gone
2383        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 0);
2384    }
2385
2386    /**
2387     * Test multiple account support.
2388     */
2389    public void testMultipleAccounts() throws Exception {
2390        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2391        int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com");
2392        Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0"));
2393        Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1"));
2394
2395        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2396        Uri eventsWithAccount = CalendarContract.Events.CONTENT_URI.buildUpon()
2397                .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_NAME, DEFAULT_ACCOUNT)
2398                .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_TYPE,
2399                        DEFAULT_ACCOUNT_TYPE)
2400                .build();
2401        // Only one event for that account
2402        testQueryCount(eventsWithAccount, null, 1);
2403
2404        // Test deletion with account and selection
2405
2406        long eventId = ContentUris.parseId(eventUri1);
2407        // Wrong account, should not be deleted
2408        assertEquals("delete", 0, mResolver.delete(
2409                updatedUri(eventsWithAccount, true /* syncAdapter */, DEFAULT_ACCOUNT,
2410                        DEFAULT_ACCOUNT_TYPE),
2411                "_id=" + eventId, null /* selectionArgs */));
2412        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2);
2413        // Right account, should be deleted
2414        assertEquals("delete", 1, mResolver.delete(
2415                updatedUri(CalendarContract.Events.CONTENT_URI, true /* syncAdapter */,
2416                        "user2@google.com", DEFAULT_ACCOUNT_TYPE),
2417                "_id=" + eventId, null /* selectionArgs */));
2418        testQueryCount(CalendarContract.Events.CONTENT_URI, null, 1);
2419    }
2420
2421    /**
2422     * Run commands, wiping instance table at each step.
2423     * This tests full instance expansion.
2424     * @throws Exception
2425     */
2426    public void testCommandSequences1() throws Exception {
2427        commandSequences(true);
2428    }
2429
2430    /**
2431     * Run commands normally.
2432     * This tests incremental instance expansion.
2433     * @throws Exception
2434     */
2435    public void testCommandSequences2() throws Exception {
2436        commandSequences(false);
2437    }
2438
2439    /**
2440     * Run thorough set of command sequences
2441     * @param wipe true if instances should be wiped and regenerated
2442     * @throws Exception
2443     */
2444    private void commandSequences(boolean wipe) throws Exception {
2445        Cursor cursor;
2446        Uri url = null;
2447        mWipe = wipe; // Set global flag
2448
2449        mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2450
2451        cursor = mResolver.query(mEventsUri, null, null, null, null);
2452        assertEquals(0, cursor.getCount());
2453        cursor.close();
2454        Command[] commands;
2455
2456        Log.i(TAG, "Normal insert/delete");
2457        commands = mNormalInsertDelete;
2458        for (Command command : commands) {
2459            command.execute();
2460        }
2461
2462        deleteAllEvents();
2463
2464        Log.i(TAG, "All-day insert/delete");
2465        commands = mAlldayInsertDelete;
2466        for (Command command : commands) {
2467            command.execute();
2468        }
2469
2470        deleteAllEvents();
2471
2472        Log.i(TAG, "Recurring insert/delete");
2473        commands = mRecurringInsertDelete;
2474        for (Command command : commands) {
2475            command.execute();
2476        }
2477
2478        deleteAllEvents();
2479
2480        Log.i(TAG, "Exception with truncated recurrence");
2481        commands = mExceptionWithTruncatedRecurrence;
2482        for (Command command : commands) {
2483            command.execute();
2484        }
2485
2486        deleteAllEvents();
2487
2488        Log.i(TAG, "Exception with moved recurrence");
2489        commands = mExceptionWithMovedRecurrence;
2490        for (Command command : commands) {
2491            command.execute();
2492        }
2493
2494        deleteAllEvents();
2495
2496        Log.i(TAG, "Exception with cancel");
2497        commands = mCancelInstance;
2498        for (Command command : commands) {
2499            command.execute();
2500        }
2501
2502        deleteAllEvents();
2503
2504        Log.i(TAG, "Exception with moved recurrence2");
2505        commands = mExceptionWithMovedRecurrence2;
2506        for (Command command : commands) {
2507            command.execute();
2508        }
2509
2510        deleteAllEvents();
2511
2512        Log.i(TAG, "Exception with no recurrence");
2513        commands = mExceptionWithNoRecurrence;
2514        for (Command command : commands) {
2515            command.execute();
2516        }
2517    }
2518
2519    /**
2520     * Test Time toString.
2521     * @throws Exception
2522     */
2523    // Suppressed because toString currently hangs.
2524    @Suppress
2525    public void testTimeToString() throws Exception {
2526        Time time = new Time(Time.TIMEZONE_UTC);
2527        String str = "2039-01-01T23:00:00.000Z";
2528        String result = "20390101T230000UTC(0,0,0,-1,0)";
2529        time.parse3339(str);
2530        assertEquals(result, time.toString());
2531    }
2532
2533    /**
2534     * Test the query done by Event.loadEvents
2535     * Also test that instance queries work when an event straddles the expansion range
2536     * @throws Exception
2537     */
2538    public void testInstanceQuery() throws Exception {
2539        final String[] PROJECTION = new String[] {
2540                Instances.TITLE,                 // 0
2541                Instances.EVENT_LOCATION,        // 1
2542                Instances.ALL_DAY,               // 2
2543                Instances.CALENDAR_COLOR,        // 3
2544                Instances.EVENT_TIMEZONE,        // 4
2545                Instances.EVENT_ID,              // 5
2546                Instances.BEGIN,                 // 6
2547                Instances.END,                   // 7
2548                Instances._ID,                   // 8
2549                Instances.START_DAY,             // 9
2550                Instances.END_DAY,               // 10
2551                Instances.START_MINUTE,          // 11
2552                Instances.END_MINUTE,            // 12
2553                Instances.HAS_ALARM,             // 13
2554                Instances.RRULE,                 // 14
2555                Instances.RDATE,                 // 15
2556                Instances.SELF_ATTENDEE_STATUS,  // 16
2557                Events.ORGANIZER,                // 17
2558                Events.GUESTS_CAN_MODIFY,        // 18
2559        };
2560
2561        String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW;
2562        String where = Instances.SELF_ATTENDEE_STATUS + "!="
2563                + CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED;
2564
2565        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2566        final String START = "2008-05-01T00:00:00";
2567        final String END = "2008-05-01T20:00:00";
2568
2569        EventInfo[] events = { new EventInfo("normal0",
2570                START,
2571                END,
2572                false /* allDay */,
2573                DEFAULT_TIMEZONE) };
2574
2575        insertEvent(calId, events[0]);
2576
2577        Time time = new Time(DEFAULT_TIMEZONE);
2578        time.parse3339(START);
2579        long startMs = time.toMillis(true /* ignoreDst */);
2580        // Query starting from way in the past to one hour into the event.
2581        // Query is more than 2 months so the range won't get extended by the provider.
2582        Cursor cursor = queryInstances(mResolver, PROJECTION,
2583                startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS,
2584                where, null, orderBy);
2585        try {
2586            assertEquals(1, cursor.getCount());
2587        } finally {
2588            cursor.close();
2589        }
2590
2591        // Now expand the instance range.  The event overlaps the new part of the range.
2592        cursor = queryInstances(mResolver, PROJECTION,
2593                startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS,
2594                where, null, orderBy);
2595        try {
2596            assertEquals(1, cursor.getCount());
2597        } finally {
2598            cursor.close();
2599        }
2600    }
2601
2602    /**
2603     * Performs a query to return all visible instances in the given range that
2604     * match the given selection. This is a blocking function and should not be
2605     * done on the UI thread. This will cause an expansion of recurring events
2606     * to fill this time range if they are not already expanded and will slow
2607     * down for larger time ranges with many recurring events.
2608     *
2609     * @param cr The ContentResolver to use for the query
2610     * @param projection The columns to return
2611     * @param begin The start of the time range to query in UTC millis since
2612     *            epoch
2613     * @param end The end of the time range to query in UTC millis since epoch
2614     * @param selection Filter on the query as an SQL WHERE statement
2615     * @param selectionArgs Args to replace any '?'s in the selection
2616     * @param orderBy How to order the rows as an SQL ORDER BY statement
2617     * @return A Cursor of instances matching the selection
2618     */
2619    private static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin,
2620            long end, String selection, String[] selectionArgs, String orderBy) {
2621
2622        Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
2623        ContentUris.appendId(builder, begin);
2624        ContentUris.appendId(builder, end);
2625        if (TextUtils.isEmpty(selection)) {
2626            selection = WHERE_CALENDARS_SELECTED;
2627            selectionArgs = WHERE_CALENDARS_ARGS;
2628        } else {
2629            selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED;
2630            if (selectionArgs != null && selectionArgs.length > 0) {
2631                selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1);
2632                selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0];
2633            } else {
2634                selectionArgs = WHERE_CALENDARS_ARGS;
2635            }
2636        }
2637        return cr.query(builder.build(), projection, selection, selectionArgs,
2638                orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
2639    }
2640
2641    /**
2642     * Performs a query to return all visible instances in the given range that
2643     * match the given selection. This is a blocking function and should not be
2644     * done on the UI thread. This will cause an expansion of recurring events
2645     * to fill this time range if they are not already expanded and will slow
2646     * down for larger time ranges with many recurring events.
2647     *
2648     * @param cr The ContentResolver to use for the query
2649     * @param projection The columns to return
2650     * @param begin The start of the time range to query in UTC millis since
2651     *            epoch
2652     * @param end The end of the time range to query in UTC millis since epoch
2653     * @param searchQuery A string of space separated search terms. Segments
2654     *            enclosed by double quotes will be treated as a single term.
2655     * @param selection Filter on the query as an SQL WHERE statement
2656     * @param selectionArgs Args to replace any '?'s in the selection
2657     * @param orderBy How to order the rows as an SQL ORDER BY statement
2658     * @return A Cursor of instances matching the selection
2659     */
2660    public static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin,
2661            long end, String searchQuery, String selection, String[] selectionArgs, String orderBy)
2662            {
2663        Uri.Builder builder = Instances.CONTENT_SEARCH_URI.buildUpon();
2664        ContentUris.appendId(builder, begin);
2665        ContentUris.appendId(builder, end);
2666        builder = builder.appendPath(searchQuery);
2667        if (TextUtils.isEmpty(selection)) {
2668            selection = WHERE_CALENDARS_SELECTED;
2669            selectionArgs = WHERE_CALENDARS_ARGS;
2670        } else {
2671            selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED;
2672            if (selectionArgs != null && selectionArgs.length > 0) {
2673                selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1);
2674                selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0];
2675            } else {
2676                selectionArgs = WHERE_CALENDARS_ARGS;
2677            }
2678        }
2679        return cr.query(builder.build(), projection, selection, selectionArgs,
2680                orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
2681    }
2682
2683    private Cursor queryInstances(long begin, long end) {
2684        Uri url = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI, begin + "/" + end);
2685        return mResolver.query(url, null, null, null, null);
2686    }
2687
2688    protected static class MockProvider extends ContentProvider {
2689
2690        private String mAuthority;
2691
2692        private int mNumItems = 0;
2693
2694        public MockProvider(String authority) {
2695            mAuthority = authority;
2696        }
2697
2698        @Override
2699        public boolean onCreate() {
2700            return true;
2701        }
2702
2703        @Override
2704        public Cursor query(Uri uri, String[] projection, String selection,
2705                String[] selectionArgs, String sortOrder) {
2706            return new MatrixCursor(new String[]{ "_id" }, 0);
2707        }
2708
2709        @Override
2710        public String getType(Uri uri) {
2711            throw new UnsupportedOperationException();
2712        }
2713
2714        @Override
2715        public Uri insert(Uri uri, ContentValues values) {
2716            mNumItems++;
2717            return Uri.parse("content://" + mAuthority + "/" + mNumItems);
2718        }
2719
2720        @Override
2721        public int delete(Uri uri, String selection, String[] selectionArgs) {
2722            return 0;
2723        }
2724
2725        @Override
2726        public int update(Uri uri, ContentValues values, String selection,
2727                String[] selectionArgs) {
2728            return 0;
2729        }
2730    }
2731
2732    private void cleanCalendarDataTable(SQLiteOpenHelper helper) {
2733        if (null == helper) {
2734            return;
2735        }
2736        SQLiteDatabase db = helper.getWritableDatabase();
2737        db.execSQL("DELETE FROM CalendarCache;");
2738    }
2739
2740    public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException {
2741        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2742        cleanCalendarDataTable(helper);
2743        CalendarCache cache = new CalendarCache(helper);
2744
2745        boolean hasException = false;
2746        try {
2747            String value = cache.readData(null);
2748        } catch (CalendarCache.CacheException e) {
2749            hasException = true;
2750        }
2751        assertTrue(hasException);
2752
2753        assertNull(cache.readTimezoneDatabaseVersion());
2754
2755        cache.writeTimezoneDatabaseVersion("1234");
2756        assertEquals("1234", cache.readTimezoneDatabaseVersion());
2757
2758        cache.writeTimezoneDatabaseVersion("5678");
2759        assertEquals("5678", cache.readTimezoneDatabaseVersion());
2760    }
2761
2762    private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) {
2763        Uri uri = Uri.parse("content://" + CalendarContract.AUTHORITY + "/events");
2764        Log.i(TAG, "Looking for EventId = " + eventId);
2765
2766        Cursor cursor = mResolver.query(uri, null, null, null, null);
2767        assertEquals(1, cursor.getCount());
2768
2769        int colIndexTitle = cursor.getColumnIndex(CalendarContract.Events.TITLE);
2770        int colIndexDtStart = cursor.getColumnIndex(CalendarContract.Events.DTSTART);
2771        int colIndexDtEnd = cursor.getColumnIndex(CalendarContract.Events.DTEND);
2772        int colIndexAllDay = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY);
2773        if (!cursor.moveToNext()) {
2774            Log.e(TAG,"Could not find inserted event");
2775            assertTrue(false);
2776        }
2777        assertEquals(title, cursor.getString(colIndexTitle));
2778        assertEquals(dtStart, cursor.getLong(colIndexDtStart));
2779        assertEquals(dtEnd, cursor.getLong(colIndexDtEnd));
2780        assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0));
2781        cursor.close();
2782    }
2783
2784    public void testChangeTimezoneDB() {
2785        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2786
2787        Cursor cursor = mResolver
2788                .query(CalendarContract.Events.CONTENT_URI, null, null, null, null);
2789        assertEquals(0, cursor.getCount());
2790        cursor.close();
2791
2792        EventInfo[] events = { new EventInfo("normal0",
2793                                        "2008-05-01T00:00:00",
2794                                        "2008-05-02T00:00:00",
2795                                        false,
2796                                        DEFAULT_TIMEZONE) };
2797
2798        Uri uri = insertEvent(calId, events[0]);
2799        assertNotNull(uri);
2800
2801        // check the inserted event
2802        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2803
2804        // inject a new time zone
2805        getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE,
2806                MOCK_TIME_ZONE_DATABASE_VERSION);
2807
2808        // check timezone database version
2809        assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion());
2810
2811        // check that the inserted event has *not* been updated
2812        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2813    }
2814
2815    public static final Uri PROPERTIES_CONTENT_URI =
2816            Uri.parse("content://" + CalendarContract.AUTHORITY + "/properties");
2817
2818    public static final int COLUMN_KEY_INDEX = 1;
2819    public static final int COLUMN_VALUE_INDEX = 0;
2820
2821    public void testGetProviderProperties() throws CalendarCache.CacheException {
2822        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2823        cleanCalendarDataTable(helper);
2824        CalendarCache cache = new CalendarCache(helper);
2825
2826        cache.writeTimezoneDatabaseVersion("2010k");
2827        cache.writeTimezoneInstances("America/Denver");
2828        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2829        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2830
2831        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, null, null, null);
2832        assertEquals(4, cursor.getCount());
2833
2834        assertEquals(CalendarCache.COLUMN_NAME_KEY, cursor.getColumnName(COLUMN_KEY_INDEX));
2835        assertEquals(CalendarCache.COLUMN_NAME_VALUE, cursor.getColumnName(COLUMN_VALUE_INDEX));
2836
2837        Map<String, String> map = new HashMap<String, String>();
2838
2839        while (cursor.moveToNext()) {
2840            String key = cursor.getString(COLUMN_KEY_INDEX);
2841            String value = cursor.getString(COLUMN_VALUE_INDEX);
2842            map.put(key, value);
2843        }
2844
2845        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2846        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_TYPE));
2847        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES));
2848        assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2849
2850        assertEquals("2010k", map.get(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION));
2851        assertEquals("America/Denver", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES));
2852        assertEquals("America/Los_Angeles", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS));
2853        assertEquals(CalendarCache.TIMEZONE_TYPE_AUTO, map.get(CalendarCache.KEY_TIMEZONE_TYPE));
2854
2855        cursor.close();
2856    }
2857
2858    public void testGetProviderPropertiesByKey() throws CalendarCache.CacheException {
2859        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2860        cleanCalendarDataTable(helper);
2861        CalendarCache cache = new CalendarCache(helper);
2862
2863        cache.writeTimezoneDatabaseVersion("2010k");
2864        cache.writeTimezoneInstances("America/Denver");
2865        cache.writeTimezoneInstancesPrevious("America/Los_Angeles");
2866        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2867
2868        checkValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2869        checkValueForKey("2010k", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2870        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2871        checkValueForKey("America/Los_Angeles", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2872    }
2873
2874    private void checkValueForKey(String value, String key) {
2875        Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null,
2876                "key=?", new String[] {key}, null);
2877
2878        assertEquals(1, cursor.getCount());
2879        assertTrue(cursor.moveToFirst());
2880        assertEquals(cursor.getString(COLUMN_KEY_INDEX), key);
2881        assertEquals(cursor.getString(COLUMN_VALUE_INDEX), value);
2882
2883        cursor.close();
2884    }
2885
2886    public void testUpdateProviderProperties() throws CalendarCache.CacheException {
2887        CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
2888        cleanCalendarDataTable(helper);
2889        CalendarCache cache = new CalendarCache(helper);
2890
2891        String localTimezone = TimeZone.getDefault().getID();
2892
2893        // Set initial value
2894        cache.writeTimezoneDatabaseVersion("2010k");
2895
2896        updateValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2897        checkValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION);
2898
2899        // Set initial values
2900        cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO);
2901        cache.writeTimezoneInstances("America/Chicago");
2902        cache.writeTimezoneInstancesPrevious("America/Denver");
2903
2904        updateValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE);
2905        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2906        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2907
2908        updateValueForKey(CalendarCache.TIMEZONE_TYPE_HOME, CalendarCache.KEY_TIMEZONE_TYPE);
2909        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES);
2910        checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2911
2912        // Set initial value
2913        cache.writeTimezoneInstancesPrevious("");
2914        updateValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2915        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES);
2916        checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS);
2917    }
2918
2919    private void updateValueForKey(String value, String key) {
2920        ContentValues contentValues = new ContentValues();
2921        contentValues.put(CalendarCache.COLUMN_NAME_VALUE, value);
2922
2923        int result = mResolver.update(PROPERTIES_CONTENT_URI,
2924                contentValues,
2925                CalendarCache.COLUMN_NAME_KEY + "=?",
2926                new String[] {key});
2927
2928        assertEquals(1, result);
2929    }
2930
2931    public void testInsertOriginalTimezoneInExtProperties() throws Exception {
2932        int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
2933
2934
2935        EventInfo[] events = { new EventInfo("normal0",
2936                                        "2008-05-01T00:00:00",
2937                                        "2008-05-02T00:00:00",
2938                                        false,
2939                                        DEFAULT_TIMEZONE) };
2940
2941        Uri eventUri = insertEvent(calId, events[0]);
2942        assertNotNull(eventUri);
2943
2944        long eventId = ContentUris.parseId(eventUri);
2945        assertTrue(eventId > -1);
2946
2947        // check the inserted event
2948        checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay);
2949
2950        // Should have 1 calendars and 1 event
2951        testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 1);
2952        testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 1);
2953
2954        // Verify that the original timezone is correct
2955        Cursor cursor = mResolver.query(CalendarContract.ExtendedProperties.CONTENT_URI,
2956                null/* projection */,
2957                "event_id=" + eventId,
2958                null /* selectionArgs */,
2959                null /* sortOrder */);
2960        try {
2961            // Should have 1 extended property for the original timezone
2962            assertEquals(1, cursor.getCount());
2963
2964            if (cursor.moveToFirst()) {
2965                long id = cursor.getLong(1);
2966                assertEquals(id, eventId);
2967
2968                assertEquals(CalendarProvider2.EXT_PROP_ORIGINAL_TIMEZONE, cursor.getString(2));
2969                assertEquals(DEFAULT_TIMEZONE, cursor.getString(3));
2970            }
2971        } finally {
2972            cursor.close();
2973        }
2974    }
2975
2976    /**
2977     * Verifies that the number of defined calendars meets expectations.
2978     *
2979     * @param expectedCount The number of calendars we expect to find.
2980     */
2981    private void checkCalendarCount(int expectedCount) {
2982        Cursor cursor = mResolver.query(mCalendarsUri,
2983                null /* projection */,
2984                null /* selection */,
2985                null /* selectionArgs */,
2986                null /* sortOrder */);
2987        assertEquals(expectedCount, cursor.getCount());
2988        cursor.close();
2989    }
2990
2991    private void checkCalendarExists(int calId) {
2992        assertTrue(isCalendarExists(calId));
2993    }
2994
2995    private void checkCalendarDoesNotExists(int calId) {
2996        assertFalse(isCalendarExists(calId));
2997    }
2998
2999    private boolean isCalendarExists(int calId) {
3000        Cursor cursor = mResolver.query(mCalendarsUri,
3001                new String[] {Calendars._ID},
3002                null /* selection */,
3003                null /* selectionArgs */,
3004                null /* sortOrder */);
3005        boolean found = false;
3006        while (cursor.moveToNext()) {
3007            if (calId == cursor.getInt(0)) {
3008                found = true;
3009                break;
3010            }
3011        }
3012        cursor.close();
3013        return found;
3014    }
3015
3016    public void testDeleteAllCalendars() {
3017        checkCalendarCount(0);
3018
3019        insertCal("Calendar1", "America/Los_Angeles");
3020        insertCal("Calendar2", "America/Los_Angeles");
3021
3022        checkCalendarCount(2);
3023
3024        deleteMatchingCalendars(null /* selection */, null /* selectionArgs*/);
3025        checkCalendarCount(0);
3026    }
3027
3028    public void testDeleteCalendarsWithSelection() {
3029        checkCalendarCount(0);
3030
3031        int calId1 = insertCal("Calendar1", "America/Los_Angeles");
3032        int calId2 = insertCal("Calendar2", "America/Los_Angeles");
3033
3034        checkCalendarCount(2);
3035        checkCalendarExists(calId1);
3036        checkCalendarExists(calId2);
3037
3038        deleteMatchingCalendars(Calendars._ID + "=" + calId2, null /* selectionArgs*/);
3039        checkCalendarCount(1);
3040        checkCalendarExists(calId1);
3041        checkCalendarDoesNotExists(calId2);
3042    }
3043
3044    public void testDeleteCalendarsWithSelectionAndArgs() {
3045        checkCalendarCount(0);
3046
3047        int calId1 = insertCal("Calendar1", "America/Los_Angeles");
3048        int calId2 = insertCal("Calendar2", "America/Los_Angeles");
3049
3050        checkCalendarCount(2);
3051        checkCalendarExists(calId1);
3052        checkCalendarExists(calId2);
3053
3054        deleteMatchingCalendars(Calendars._ID + "=?",
3055                new String[] { Integer.toString(calId2) });
3056        checkCalendarCount(1);
3057        checkCalendarExists(calId1);
3058        checkCalendarDoesNotExists(calId2);
3059
3060        deleteMatchingCalendars(Calendars._ID + "=?" + " AND " + Calendars.NAME + "=?",
3061                new String[] { Integer.toString(calId1), "Calendar1" });
3062        checkCalendarCount(0);
3063    }
3064}
3065