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