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