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