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