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