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