Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2011 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 android.provider.cts;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Entity;
     25 import android.content.EntityIterator;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.cts.util.PollingCheck;
     29 import android.database.Cursor;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.os.Environment;
     33 import android.provider.CalendarContract;
     34 import android.provider.CalendarContract.Attendees;
     35 import android.provider.CalendarContract.CalendarEntity;
     36 import android.provider.CalendarContract.Calendars;
     37 import android.provider.CalendarContract.Colors;
     38 import android.provider.CalendarContract.Events;
     39 import android.provider.CalendarContract.EventsEntity;
     40 import android.provider.CalendarContract.ExtendedProperties;
     41 import android.provider.CalendarContract.Instances;
     42 import android.provider.CalendarContract.Reminders;
     43 import android.provider.CalendarContract.SyncState;
     44 import android.test.InstrumentationCtsTestRunner;
     45 import android.test.InstrumentationTestCase;
     46 import android.test.suitebuilder.annotation.MediumTest;
     47 import android.text.TextUtils;
     48 import android.text.format.DateUtils;
     49 import android.text.format.Time;
     50 import android.util.Log;
     51 
     52 import java.util.ArrayList;
     53 import java.util.HashSet;
     54 import java.util.List;
     55 import java.util.Set;
     56 
     57 public class CalendarTest extends InstrumentationTestCase {
     58 
     59     private static final String TAG = "CalCTS";
     60     private static final String CTS_TEST_TYPE = "LOCAL";
     61 
     62     // an arbitrary int used by some tests
     63     private static final int SOME_ARBITRARY_INT = 143234;
     64 
     65     // 10 sec timeout for reminder broadcast (but shouldn't usually take this long).
     66     private static final int POLLING_TIMEOUT = 10000;
     67 
     68     // @formatter:off
     69     private static final String[] TIME_ZONES = new String[] {
     70             "UTC",
     71             "America/Los_Angeles",
     72             "Asia/Beirut",
     73             "Pacific/Auckland", };
     74     // @formatter:on
     75 
     76     private static final String SQL_WHERE_ID = Events._ID + "=?";
     77     private static final String SQL_WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
     78 
     79     private ContentResolver mContentResolver;
     80 
     81     /** If set, log verbose instance info when running recurrence tests. */
     82     private static final boolean DEBUG_RECURRENCE = false;
     83 
     84     private static class CalendarHelper {
     85 
     86         // @formatter:off
     87         public static final String[] CALENDARS_SYNC_PROJECTION = new String[] {
     88                 Calendars._ID,
     89                 Calendars.ACCOUNT_NAME,
     90                 Calendars.ACCOUNT_TYPE,
     91                 Calendars._SYNC_ID,
     92                 Calendars.CAL_SYNC7,
     93                 Calendars.CAL_SYNC8,
     94                 Calendars.DIRTY,
     95                 Calendars.NAME,
     96                 Calendars.CALENDAR_DISPLAY_NAME,
     97                 Calendars.CALENDAR_COLOR,
     98                 Calendars.CALENDAR_COLOR_KEY,
     99                 Calendars.CALENDAR_ACCESS_LEVEL,
    100                 Calendars.VISIBLE,
    101                 Calendars.SYNC_EVENTS,
    102                 Calendars.CALENDAR_LOCATION,
    103                 Calendars.CALENDAR_TIME_ZONE,
    104                 Calendars.OWNER_ACCOUNT,
    105                 Calendars.CAN_ORGANIZER_RESPOND,
    106                 Calendars.CAN_MODIFY_TIME_ZONE,
    107                 Calendars.MAX_REMINDERS,
    108                 Calendars.ALLOWED_REMINDERS,
    109                 Calendars.ALLOWED_AVAILABILITY,
    110                 Calendars.ALLOWED_ATTENDEE_TYPES,
    111                 Calendars.DELETED,
    112                 Calendars.CAL_SYNC1,
    113                 Calendars.CAL_SYNC2,
    114                 Calendars.CAL_SYNC3,
    115                 Calendars.CAL_SYNC4,
    116                 Calendars.CAL_SYNC5,
    117                 Calendars.CAL_SYNC6,
    118                 };
    119         // @formatter:on
    120 
    121         private CalendarHelper() {}     // do not instantiate this class
    122 
    123         /**
    124          * Generates the e-mail address for the Calendar owner.  Use this for
    125          * Calendars.OWNER_ACCOUNT, Events.OWNER_ACCOUNT, and for Attendees.ATTENDEE_EMAIL
    126          * when you want a "self" attendee entry.
    127          */
    128         static String generateCalendarOwnerEmail(String account) {
    129             return "OWNER_" + account + "@example.com";
    130         }
    131 
    132         /**
    133          * Creates a new set of values for creating a single calendar with every
    134          * field.
    135          *
    136          * @param account The account name to create this calendar with
    137          * @param seed A number used to generate the values
    138          * @return A complete set of values for the calendar
    139          */
    140         public static ContentValues getNewCalendarValues(
    141                 String account, int seed) {
    142             String seedString = Long.toString(seed);
    143             ContentValues values = new ContentValues();
    144             values.put(Calendars.ACCOUNT_TYPE, CTS_TEST_TYPE);
    145 
    146             values.put(Calendars.ACCOUNT_NAME, account);
    147             values.put(Calendars._SYNC_ID, "SYNC_ID:" + seedString);
    148             values.put(Calendars.CAL_SYNC7, "SYNC_V:" + seedString);
    149             values.put(Calendars.CAL_SYNC8, "SYNC_TIME:" + seedString);
    150             values.put(Calendars.DIRTY, 0);
    151             values.put(Calendars.OWNER_ACCOUNT, generateCalendarOwnerEmail(account));
    152 
    153             values.put(Calendars.NAME, seedString);
    154             values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString);
    155 
    156             values.put(Calendars.CALENDAR_ACCESS_LEVEL, (seed % 8) * 100);
    157 
    158             values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed);
    159             values.put(Calendars.VISIBLE, seed % 2);
    160             values.put(Calendars.SYNC_EVENTS, 1);   // must be 1 for recurrence expansion
    161             values.put(Calendars.CALENDAR_LOCATION, "LOCATION:" + seedString);
    162             values.put(Calendars.CALENDAR_TIME_ZONE, TIME_ZONES[seed % TIME_ZONES.length]);
    163             values.put(Calendars.CAN_ORGANIZER_RESPOND, seed % 2);
    164             values.put(Calendars.CAN_MODIFY_TIME_ZONE, seed % 2);
    165             values.put(Calendars.MAX_REMINDERS, 3);
    166             values.put(Calendars.ALLOWED_REMINDERS, "0,1,2");   // does not include SMS (3)
    167             values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "0,1,2,3");
    168             values.put(Calendars.ALLOWED_AVAILABILITY, "0,1,2,3");
    169             values.put(Calendars.CAL_SYNC1, "SYNC1:" + seedString);
    170             values.put(Calendars.CAL_SYNC2, "SYNC2:" + seedString);
    171             values.put(Calendars.CAL_SYNC3, "SYNC3:" + seedString);
    172             values.put(Calendars.CAL_SYNC4, "SYNC4:" + seedString);
    173             values.put(Calendars.CAL_SYNC5, "SYNC5:" + seedString);
    174             values.put(Calendars.CAL_SYNC6, "SYNC6:" + seedString);
    175 
    176             return values;
    177         }
    178 
    179         /**
    180          * Creates a set of values with just the updates and modifies the
    181          * original values to the expected values
    182          */
    183         public static ContentValues getUpdateCalendarValuesWithOriginal(
    184                 ContentValues original, int seed) {
    185             ContentValues values = new ContentValues();
    186             String seedString = Long.toString(seed);
    187 
    188             values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString);
    189             values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed);
    190             values.put(Calendars.VISIBLE, seed % 2);
    191             values.put(Calendars.SYNC_EVENTS, seed % 2);
    192 
    193             original.putAll(values);
    194             original.put(Calendars.DIRTY, 1);
    195 
    196             return values;
    197         }
    198 
    199         public static int deleteCalendarById(ContentResolver resolver, long id) {
    200             return resolver.delete(Calendars.CONTENT_URI, Calendars._ID + "=?",
    201                     new String[] { Long.toString(id) });
    202         }
    203 
    204         public static int deleteCalendarByAccount(ContentResolver resolver, String account) {
    205             return resolver.delete(Calendars.CONTENT_URI, Calendars.ACCOUNT_NAME + "=?",
    206                     new String[] { account });
    207         }
    208 
    209         public static Cursor getCalendarsByAccount(ContentResolver resolver, String account) {
    210             String selection = Calendars.ACCOUNT_TYPE + "=?";
    211             String[] selectionArgs;
    212             if (account != null) {
    213                 selection += " AND " + Calendars.ACCOUNT_NAME + "=?";
    214                 selectionArgs = new String[2];
    215                 selectionArgs[1] = account;
    216             } else {
    217                 selectionArgs = new String[1];
    218             }
    219             selectionArgs[0] = CTS_TEST_TYPE;
    220 
    221             return resolver.query(Calendars.CONTENT_URI, CALENDARS_SYNC_PROJECTION, selection,
    222                     selectionArgs, null);
    223         }
    224     }
    225 
    226     /**
    227      * Helper class for manipulating entries in the _sync_state table.
    228      */
    229     private static class SyncStateHelper {
    230         public static final String[] SYNCSTATE_PROJECTION = new String[] {
    231             SyncState._ID,
    232             SyncState.ACCOUNT_NAME,
    233             SyncState.ACCOUNT_TYPE,
    234             SyncState.DATA
    235         };
    236 
    237         private static final byte[] SAMPLE_SYNC_DATA = {
    238             (byte) 'H', (byte) 'e', (byte) 'l', (byte) 'l', (byte) 'o'
    239         };
    240 
    241         private SyncStateHelper() {}      // do not instantiate
    242 
    243         /**
    244          * Creates a new set of values for creating a new _sync_state entry.
    245          */
    246         public static ContentValues getNewSyncStateValues(String account) {
    247             ContentValues values = new ContentValues();
    248             values.put(SyncState.DATA, SAMPLE_SYNC_DATA);
    249             values.put(SyncState.ACCOUNT_NAME, account);
    250             values.put(SyncState.ACCOUNT_TYPE, CTS_TEST_TYPE);
    251             return values;
    252         }
    253 
    254         /**
    255          * Retrieves the _sync_state entry with the specified ID.
    256          */
    257         public static Cursor getSyncStateById(ContentResolver resolver, long id) {
    258             Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id);
    259             return resolver.query(uri, SYNCSTATE_PROJECTION, null, null, null);
    260         }
    261 
    262         /**
    263          * Retrieves the _sync_state entry for the specified account.
    264          */
    265         public static Cursor getSyncStateByAccount(ContentResolver resolver, String account) {
    266             assertNotNull(account);
    267             String selection = SyncState.ACCOUNT_TYPE + "=? AND " + SyncState.ACCOUNT_NAME + "=?";
    268             String[] selectionArgs = new String[] { CTS_TEST_TYPE, account };
    269 
    270             return resolver.query(SyncState.CONTENT_URI, SYNCSTATE_PROJECTION, selection,
    271                     selectionArgs, null);
    272         }
    273 
    274         /**
    275          * Deletes the _sync_state entry with the specified ID.  Always done as app.
    276          */
    277         public static int deleteSyncStateById(ContentResolver resolver, long id) {
    278             Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id);
    279             return resolver.delete(uri, null, null);
    280         }
    281 
    282         /**
    283          * Deletes the _sync_state entry associated with the specified account.  Can be done
    284          * as app or sync adapter.
    285          */
    286         public static int deleteSyncStateByAccount(ContentResolver resolver, String account,
    287                 boolean asSyncAdapter) {
    288             Uri uri = SyncState.CONTENT_URI;
    289             if (asSyncAdapter) {
    290                 uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
    291             }
    292             return resolver.delete(uri, SyncState.ACCOUNT_NAME + "=?",
    293                     new String[] { account });
    294         }
    295     }
    296 
    297     // @formatter:off
    298     private static class EventHelper {
    299         public static final String[] EVENTS_PROJECTION = new String[] {
    300             Events._ID,
    301             Events.ACCOUNT_NAME,
    302             Events.ACCOUNT_TYPE,
    303             Events.OWNER_ACCOUNT,
    304             // Events.ORGANIZER_CAN_RESPOND, from Calendars
    305             // Events.CAN_CHANGE_TZ, from Calendars
    306             // Events.MAX_REMINDERS, from Calendars
    307             Events.CALENDAR_ID,
    308             // Events.CALENDAR_DISPLAY_NAME, from Calendars
    309             // Events.CALENDAR_COLOR, from Calendars
    310             // Events.CALENDAR_ACL, from Calendars
    311             // Events.CALENDAR_VISIBLE, from Calendars
    312             Events.SYNC_DATA3,
    313             Events.SYNC_DATA6,
    314             Events.TITLE,
    315             Events.EVENT_LOCATION,
    316             Events.DESCRIPTION,
    317             Events.STATUS,
    318             Events.SELF_ATTENDEE_STATUS,
    319             Events.DTSTART,
    320             Events.DTEND,
    321             Events.EVENT_TIMEZONE,
    322             Events.EVENT_END_TIMEZONE,
    323             Events.EVENT_COLOR,
    324             Events.EVENT_COLOR_KEY,
    325             Events.DURATION,
    326             Events.ALL_DAY,
    327             Events.ACCESS_LEVEL,
    328             Events.AVAILABILITY,
    329             Events.HAS_ALARM,
    330             Events.HAS_EXTENDED_PROPERTIES,
    331             Events.RRULE,
    332             Events.RDATE,
    333             Events.EXRULE,
    334             Events.EXDATE,
    335             Events.ORIGINAL_ID,
    336             Events.ORIGINAL_SYNC_ID,
    337             Events.ORIGINAL_INSTANCE_TIME,
    338             Events.ORIGINAL_ALL_DAY,
    339             Events.LAST_DATE,
    340             Events.HAS_ATTENDEE_DATA,
    341             Events.GUESTS_CAN_MODIFY,
    342             Events.GUESTS_CAN_INVITE_OTHERS,
    343             Events.GUESTS_CAN_SEE_GUESTS,
    344             Events.ORGANIZER,
    345             Events.DELETED,
    346             Events._SYNC_ID,
    347             Events.SYNC_DATA4,
    348             Events.SYNC_DATA5,
    349             Events.DIRTY,
    350             Events.SYNC_DATA8,
    351             Events.SYNC_DATA2,
    352             Events.SYNC_DATA1,
    353             Events.SYNC_DATA2,
    354             Events.SYNC_DATA3,
    355             Events.SYNC_DATA4,
    356         };
    357         // @formatter:on
    358 
    359         private EventHelper() {}    // do not instantiate this class
    360 
    361         /**
    362          * Constructs a set of name/value pairs that can be used to create a Calendar event.
    363          * Various fields are generated from the seed value.
    364          */
    365         public static ContentValues getNewEventValues(
    366                 String account, int seed, long calendarId, boolean asSyncAdapter) {
    367             String seedString = Long.toString(seed);
    368             ContentValues values = new ContentValues();
    369             values.put(Events.ORGANIZER, "ORGANIZER:" + seedString);
    370 
    371             values.put(Events.TITLE, "TITLE:" + seedString);
    372             values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString);
    373 
    374             values.put(Events.CALENDAR_ID, calendarId);
    375 
    376             values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString);
    377             values.put(Events.STATUS, seed % 2);    // avoid STATUS_CANCELED for general testing
    378 
    379             values.put(Events.DTSTART, seed);
    380             values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS);
    381             values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]);
    382             values.put(Events.EVENT_COLOR, seed);
    383             // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) %
    384             // TIME_ZONES.length]);
    385             if ((seed % 2) == 0) {
    386                 // Either set to zero, or leave unset to get default zero.
    387                 // Must be 0 or dtstart/dtend will get adjusted.
    388                 values.put(Events.ALL_DAY, 0);
    389             }
    390             values.put(Events.ACCESS_LEVEL, seed % 4);
    391             values.put(Events.AVAILABILITY, seed % 2);
    392             values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2);
    393             values.put(Events.HAS_ATTENDEE_DATA, seed % 2);
    394             values.put(Events.GUESTS_CAN_MODIFY, seed % 2);
    395             values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2);
    396             values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2);
    397 
    398             // Default is STATUS_TENTATIVE (0).  We either set it to that explicitly, or leave
    399             // it set to the default.
    400             if (seed != Events.STATUS_TENTATIVE) {
    401                 values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE);
    402             }
    403 
    404             if (asSyncAdapter) {
    405                 values.put(Events._SYNC_ID, "SYNC_ID:" + seedString);
    406                 values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString);
    407                 values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString);
    408                 values.put(Events.SYNC_DATA3, "HTML:" + seedString);
    409                 values.put(Events.SYNC_DATA6, "COMMENTS:" + seedString);
    410                 values.put(Events.DIRTY, 0);
    411                 values.put(Events.SYNC_DATA8, "0");
    412             } else {
    413                 // only the sync adapter can set the DIRTY flag
    414                 //values.put(Events.DIRTY, 1);
    415             }
    416             // values.put(Events.SYNC1, "SYNC1:" + seedString);
    417             // values.put(Events.SYNC2, "SYNC2:" + seedString);
    418             // values.put(Events.SYNC3, "SYNC3:" + seedString);
    419             // values.put(Events.SYNC4, "SYNC4:" + seedString);
    420             // values.put(Events.SYNC5, "SYNC5:" + seedString);
    421 //            Events.RRULE,
    422 //            Events.RDATE,
    423 //            Events.EXRULE,
    424 //            Events.EXDATE,
    425 //            // Events.ORIGINAL_ID
    426 //            Events.ORIGINAL_EVENT, // rename ORIGINAL_SYNC_ID
    427 //            Events.ORIGINAL_INSTANCE_TIME,
    428 //            Events.ORIGINAL_ALL_DAY,
    429 
    430             return values;
    431         }
    432 
    433         /**
    434          * Constructs a set of name/value pairs that can be used to create a recurring
    435          * Calendar event.
    436          *
    437          * A duration of "P1D" is treated as an all-day event.
    438          *
    439          * @param startWhen Starting date/time in RFC 3339 format
    440          * @param duration Event duration, in RFC 2445 duration format
    441          * @param rrule Recurrence rule
    442          * @return name/value pairs to use when creating event
    443          */
    444         public static ContentValues getNewRecurringEventValues(String account, int seed,
    445                 long calendarId, boolean asSyncAdapter, String startWhen, String duration,
    446                 String rrule) {
    447 
    448             // Set up some general stuff.
    449             ContentValues values = getNewEventValues(account, seed, calendarId, asSyncAdapter);
    450 
    451             // Replace the DTSTART field.
    452             String timeZone = values.getAsString(Events.EVENT_TIMEZONE);
    453             Time time = new Time(timeZone);
    454             time.parse3339(startWhen);
    455             values.put(Events.DTSTART, time.toMillis(false));
    456 
    457             // Add in the recurrence-specific fields, and drop DTEND.
    458             values.put(Events.RRULE, rrule);
    459             values.put(Events.DURATION, duration);
    460             values.remove(Events.DTEND);
    461 
    462             return values;
    463         }
    464 
    465         /**
    466          * Constructs the basic name/value pairs required for an exception to a recurring event.
    467          *
    468          * @param instanceStartMillis The start time of the instance
    469          * @return name/value pairs to use when creating event
    470          */
    471         public static ContentValues getNewExceptionValues(long instanceStartMillis) {
    472             ContentValues values = new ContentValues();
    473             values.put(Events.ORIGINAL_INSTANCE_TIME, instanceStartMillis);
    474 
    475             return values;
    476         }
    477 
    478         public static ContentValues getUpdateEventValuesWithOriginal(ContentValues original,
    479                 int seed, boolean asSyncAdapter) {
    480             String seedString = Long.toString(seed);
    481             ContentValues values = new ContentValues();
    482 
    483             values.put(Events.TITLE, "TITLE:" + seedString);
    484             values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString);
    485             values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString);
    486             values.put(Events.STATUS, seed % 3);
    487 
    488             values.put(Events.DTSTART, seed);
    489             values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS);
    490             values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]);
    491             // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) %
    492             // TIME_ZONES.length]);
    493             values.put(Events.ACCESS_LEVEL, seed % 4);
    494             values.put(Events.AVAILABILITY, seed % 2);
    495             values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2);
    496             values.put(Events.HAS_ATTENDEE_DATA, seed % 2);
    497             values.put(Events.GUESTS_CAN_MODIFY, seed % 2);
    498             values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2);
    499             values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2);
    500             if (asSyncAdapter) {
    501                 values.put(Events._SYNC_ID, "SYNC_ID:" + seedString);
    502                 values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString);
    503                 values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString);
    504                 values.put(Events.DIRTY, 0);
    505             }
    506             original.putAll(values);
    507             return values;
    508         }
    509 
    510         public static void addDefaultReadOnlyValues(ContentValues values, String account,
    511                 boolean asSyncAdapter) {
    512             values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE);
    513             values.put(Events.DELETED, 0);
    514             values.put(Events.DIRTY, asSyncAdapter ? 0 : 1);
    515             values.put(Events.OWNER_ACCOUNT, CalendarHelper.generateCalendarOwnerEmail(account));
    516             values.put(Events.ACCOUNT_TYPE, CTS_TEST_TYPE);
    517             values.put(Events.ACCOUNT_NAME, account);
    518         }
    519 
    520         /**
    521          * Generates a RFC2445-format duration string.
    522          */
    523         private static String generateDurationString(long durationMillis, boolean isAllDay) {
    524             long durationSeconds = durationMillis / 1000;
    525 
    526             // The server may react differently to an all-day event specified as "P1D" than
    527             // it will to "PT86400S"; see b/1594638.
    528             if (isAllDay && (durationSeconds % 86400) == 0) {
    529                 return "P" + durationSeconds / 86400 + "D";
    530             } else {
    531                 return "PT" + durationSeconds + "S";
    532             }
    533         }
    534 
    535         /**
    536          * Deletes the event, and updates the values.
    537          * @param resolver The resolver to issue the query against.
    538          * @param uri The deletion URI.
    539          * @param values Set of values to update (sets DELETED and DIRTY).
    540          * @return The number of rows modified.
    541          */
    542         public static int deleteEvent(ContentResolver resolver, Uri uri, ContentValues values) {
    543             values.put(Events.DELETED, 1);
    544             values.put(Events.DIRTY, 1);
    545             return resolver.delete(uri, null, null);
    546         }
    547 
    548         public static int deleteEventAsSyncAdapter(ContentResolver resolver, Uri uri,
    549                 String account) {
    550             Uri syncUri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
    551             return resolver.delete(syncUri, null, null);
    552         }
    553 
    554         public static Cursor getEventsByAccount(ContentResolver resolver, String account) {
    555             String selection = Calendars.ACCOUNT_TYPE + "=?";
    556             String[] selectionArgs;
    557             if (account != null) {
    558                 selection += " AND " + Calendars.ACCOUNT_NAME + "=?";
    559                 selectionArgs = new String[2];
    560                 selectionArgs[1] = account;
    561             } else {
    562                 selectionArgs = new String[1];
    563             }
    564             selectionArgs[0] = CTS_TEST_TYPE;
    565             return resolver.query(Events.CONTENT_URI, EVENTS_PROJECTION, selection, selectionArgs,
    566                     null);
    567         }
    568 
    569         public static Cursor getEventByUri(ContentResolver resolver, Uri uri) {
    570             return resolver.query(uri, EVENTS_PROJECTION, null, null, null);
    571         }
    572 
    573         /**
    574          * Looks up the specified Event in the database and returns the "selfAttendeeStatus"
    575          * value.
    576          */
    577         public static int lookupSelfAttendeeStatus(ContentResolver resolver, long eventId) {
    578             return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId,
    579                     Events.SELF_ATTENDEE_STATUS);
    580         }
    581 
    582         /**
    583          * Looks up the specified Event in the database and returns the "hasAlarm"
    584          * value.
    585          */
    586         public static int lookupHasAlarm(ContentResolver resolver, long eventId) {
    587             return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId,
    588                     Events.HAS_ALARM);
    589         }
    590     }
    591 
    592     /**
    593      * Helper class for manipulating entries in the Attendees table.
    594      */
    595     private static class AttendeeHelper {
    596         public static final String[] ATTENDEES_PROJECTION = new String[] {
    597             Attendees._ID,
    598             Attendees.EVENT_ID,
    599             Attendees.ATTENDEE_NAME,
    600             Attendees.ATTENDEE_EMAIL,
    601             Attendees.ATTENDEE_STATUS,
    602             Attendees.ATTENDEE_RELATIONSHIP,
    603             Attendees.ATTENDEE_TYPE
    604         };
    605         // indexes into projection
    606         public static final int ATTENDEES_ID_INDEX = 0;
    607         public static final int ATTENDEES_EVENT_ID_INDEX = 1;
    608 
    609         // do not instantiate
    610         private AttendeeHelper() {}
    611 
    612         /**
    613          * Adds a new attendee to the specified event.
    614          *
    615          * @return the _id of the new attendee, or -1 on failure
    616          */
    617         public static long addAttendee(ContentResolver resolver, long eventId, String name,
    618                 String email, int status, int relationship, int type) {
    619             Uri uri = Attendees.CONTENT_URI;
    620 
    621             ContentValues attendee = new ContentValues();
    622             attendee.put(Attendees.EVENT_ID, eventId);
    623             attendee.put(Attendees.ATTENDEE_NAME, name);
    624             attendee.put(Attendees.ATTENDEE_EMAIL, email);
    625             attendee.put(Attendees.ATTENDEE_STATUS, status);
    626             attendee.put(Attendees.ATTENDEE_RELATIONSHIP, relationship);
    627             attendee.put(Attendees.ATTENDEE_TYPE, type);
    628             Uri result = resolver.insert(uri, attendee);
    629             return ContentUris.parseId(result);
    630         }
    631 
    632         /**
    633          * Finds all Attendees rows for the specified event and email address.  The returned
    634          * cursor will use {@link AttendeeHelper#ATTENDEES_PROJECTION}.
    635          */
    636         public static Cursor findAttendeesByEmail(ContentResolver resolver, long eventId,
    637                 String email) {
    638             return resolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION,
    639                     Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_EMAIL + "=?",
    640                     new String[] { String.valueOf(eventId), email }, null);
    641         }
    642     }
    643 
    644     /**
    645      * Helper class for manipulating entries in the Colors table.
    646      */
    647     private static class ColorHelper {
    648         public static final String WHERE_COLOR_ACCOUNT = Colors.ACCOUNT_NAME + "=? AND "
    649                 + Colors.ACCOUNT_TYPE + "=?";
    650         public static final String WHERE_COLOR_ACCOUNT_AND_INDEX = WHERE_COLOR_ACCOUNT + " AND "
    651                 + Colors.COLOR_KEY + "=?";
    652 
    653         public static final String[] COLORS_PROJECTION = new String[] {
    654                 Colors._ID, // 0
    655                 Colors.ACCOUNT_NAME, // 1
    656                 Colors.ACCOUNT_TYPE, // 2
    657                 Colors.DATA, // 3
    658                 Colors.COLOR_TYPE, // 4
    659                 Colors.COLOR_KEY, // 5
    660                 Colors.COLOR, // 6
    661         };
    662         // indexes into projection
    663         public static final int COLORS_ID_INDEX = 0;
    664         public static final int COLORS_INDEX_INDEX = 5;
    665         public static final int COLORS_COLOR_INDEX = 6;
    666 
    667         public static final int[] DEFAULT_TYPES = new int[] {
    668                 Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR,
    669                 Colors.TYPE_CALENDAR, Colors.TYPE_EVENT, Colors.TYPE_EVENT, Colors.TYPE_EVENT,
    670                 Colors.TYPE_EVENT,
    671         };
    672         public static final int[] DEFAULT_COLORS = new int[] {
    673                 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFAA00AA, 0xFF00AAAA, 0xFF333333, 0xFFAAAA00,
    674                 0xFFAAAAAA,
    675         };
    676         public static final String[] DEFAULT_INDICES = new String[] {
    677                 "000", "001", "010", "011", "100", "101", "110", "111",
    678         };
    679 
    680         public static final int C_COLOR_0 = 0;
    681         public static final int C_COLOR_1 = 1;
    682         public static final int C_COLOR_2 = 2;
    683         public static final int C_COLOR_3 = 3;
    684         public static final int E_COLOR_0 = 4;
    685         public static final int E_COLOR_1 = 5;
    686         public static final int E_COLOR_2 = 6;
    687         public static final int E_COLOR_3 = 7;
    688 
    689         // do not instantiate
    690         private ColorHelper() {
    691         }
    692 
    693         /**
    694          * Adds a new color to the colors table.
    695          *
    696          * @return the _id of the new color, or -1 on failure
    697          */
    698         public static long addColor(ContentResolver resolver, String accountName,
    699                 String accountType, String data, String index, int type, int color) {
    700             Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType);
    701 
    702             ContentValues colorValues = new ContentValues();
    703             colorValues.put(Colors.DATA, data);
    704             colorValues.put(Colors.COLOR_KEY, index);
    705             colorValues.put(Colors.COLOR_TYPE, type);
    706             colorValues.put(Colors.COLOR, color);
    707             Uri result = resolver.insert(uri, colorValues);
    708             return ContentUris.parseId(result);
    709         }
    710 
    711         /**
    712          * Finds the color specified by an account name/type and a color index.
    713          * The returned cursor will use {@link ColorHelper#COLORS_PROJECTION}.
    714          */
    715         public static Cursor findColorByIndex(ContentResolver resolver, String accountName,
    716                 String accountType, String index) {
    717             return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION,
    718                     WHERE_COLOR_ACCOUNT_AND_INDEX,
    719                     new String[] {accountName, accountType, index}, null);
    720         }
    721 
    722         public static Cursor findColorsByAccount(ContentResolver resolver, String accountName,
    723                 String accountType) {
    724             return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION, WHERE_COLOR_ACCOUNT,
    725                     new String[] { accountName, accountType }, null);
    726         }
    727 
    728         /**
    729          * Adds a default set of test colors to the Colors table under the given
    730          * account.
    731          *
    732          * @return true if the default colors were added successfully
    733          */
    734         public static boolean addDefaultColorsToAccount(ContentResolver resolver,
    735                 String accountName, String accountType) {
    736             for (int i = 0; i < DEFAULT_INDICES.length; i++) {
    737                 long id = addColor(resolver, accountName, accountType, null, DEFAULT_INDICES[i],
    738                         DEFAULT_TYPES[i], DEFAULT_COLORS[i]);
    739                 if (id == -1) {
    740                     return false;
    741                 }
    742             }
    743             return true;
    744         }
    745 
    746         public static void deleteColorsByAccount(ContentResolver resolver, String accountName,
    747                 String accountType) {
    748             Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType);
    749             resolver.delete(uri, WHERE_COLOR_ACCOUNT, new String[] { accountName, accountType });
    750         }
    751     }
    752 
    753 
    754     /**
    755      * Helper class for manipulating entries in the Reminders table.
    756      */
    757     private static class ReminderHelper {
    758         public static final String[] REMINDERS_PROJECTION = new String[] {
    759             Reminders._ID,
    760             Reminders.EVENT_ID,
    761             Reminders.MINUTES,
    762             Reminders.METHOD
    763         };
    764         // indexes into projection
    765         public static final int REMINDERS_ID_INDEX = 0;
    766         public static final int REMINDERS_EVENT_ID_INDEX = 1;
    767         public static final int REMINDERS_MINUTES_INDEX = 2;
    768         public static final int REMINDERS_METHOD_INDEX = 3;
    769 
    770         // do not instantiate
    771         private ReminderHelper() {}
    772 
    773         /**
    774          * Adds a new reminder to the specified event.
    775          *
    776          * @return the _id of the new reminder, or -1 on failure
    777          */
    778         public static long addReminder(ContentResolver resolver, long eventId, int minutes,
    779                 int method) {
    780             Uri uri = Reminders.CONTENT_URI;
    781 
    782             ContentValues reminder = new ContentValues();
    783             reminder.put(Reminders.EVENT_ID, eventId);
    784             reminder.put(Reminders.MINUTES, minutes);
    785             reminder.put(Reminders.METHOD, method);
    786             Uri result = resolver.insert(uri, reminder);
    787             return ContentUris.parseId(result);
    788         }
    789 
    790         /**
    791          * Finds all Reminders rows for the specified event.  The returned cursor will use
    792          * {@link ReminderHelper#REMINDERS_PROJECTION}.
    793          */
    794         public static Cursor findRemindersByEventId(ContentResolver resolver, long eventId) {
    795             return resolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION,
    796                     Reminders.EVENT_ID + "=?", new String[] { String.valueOf(eventId) }, null);
    797         }
    798 
    799         /**
    800          * Looks up the specified Reminders row and returns the "method" value.
    801          */
    802         public static int lookupMethod(ContentResolver resolver, long remId) {
    803             return getIntFromDatabase(resolver, Reminders.CONTENT_URI, remId,
    804                     Reminders.METHOD);
    805         }
    806    }
    807 
    808     /**
    809      * Helper class for manipulating entries in the ExtendedProperties table.
    810      */
    811     private static class ExtendedPropertiesHelper {
    812         public static final String[] EXTENDED_PROPERTIES_PROJECTION = new String[] {
    813             ExtendedProperties._ID,
    814             ExtendedProperties.EVENT_ID,
    815             ExtendedProperties.NAME,
    816             ExtendedProperties.VALUE
    817         };
    818         // indexes into projection
    819         public static final int EXTENDED_PROPERTIES_ID_INDEX = 0;
    820         public static final int EXTENDED_PROPERTIES_EVENT_ID_INDEX = 1;
    821         public static final int EXTENDED_PROPERTIES_NAME_INDEX = 2;
    822         public static final int EXTENDED_PROPERTIES_VALUE_INDEX = 3;
    823 
    824         // do not instantiate
    825         private ExtendedPropertiesHelper() {}
    826 
    827         /**
    828          * Adds a new ExtendedProperty for the specified event.  Runs as sync adapter.
    829          *
    830          * @return the _id of the new ExtendedProperty, or -1 on failure
    831          */
    832         public static long addExtendedProperty(ContentResolver resolver, String account,
    833                 long eventId, String name, String value) {
    834              Uri uri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE);
    835 
    836             ContentValues ep = new ContentValues();
    837             ep.put(ExtendedProperties.EVENT_ID, eventId);
    838             ep.put(ExtendedProperties.NAME, name);
    839             ep.put(ExtendedProperties.VALUE, value);
    840             Uri result = resolver.insert(uri, ep);
    841             return ContentUris.parseId(result);
    842         }
    843 
    844         /**
    845          * Finds all ExtendedProperties rows for the specified event.  The returned cursor will
    846          * use {@link ExtendedPropertiesHelper#EXTENDED_PROPERTIES_PROJECTION}.
    847          */
    848         public static Cursor findExtendedPropertiesByEventId(ContentResolver resolver,
    849                 long eventId) {
    850             return resolver.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROPERTIES_PROJECTION,
    851                     ExtendedProperties.EVENT_ID + "=?",
    852                     new String[] { String.valueOf(eventId) }, null);
    853         }
    854 
    855         /**
    856          * Finds an ExtendedProperties entry with a matching name for the specified event, and
    857          * returns the value.  Throws an exception if we don't find exactly one row.
    858          */
    859         public static String lookupValueByName(ContentResolver resolver, long eventId,
    860                 String name) {
    861             Cursor cursor = resolver.query(ExtendedProperties.CONTENT_URI,
    862                     EXTENDED_PROPERTIES_PROJECTION,
    863                     ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?",
    864                     new String[] { String.valueOf(eventId), name }, null);
    865 
    866             try {
    867                 if (cursor.getCount() != 1) {
    868                     throw new RuntimeException("Got " + cursor.getCount() + " results, expected 1");
    869                 }
    870 
    871                 cursor.moveToFirst();
    872                 return cursor.getString(EXTENDED_PROPERTIES_VALUE_INDEX);
    873             } finally {
    874                 if (cursor != null) {
    875                     cursor.close();
    876                 }
    877             }
    878         }
    879     }
    880 
    881     /**
    882      * Creates an updated URI that includes query parameters that identify the source as a
    883      * sync adapter.
    884      */
    885     static Uri asSyncAdapter(Uri uri, String account, String accountType) {
    886         return uri.buildUpon()
    887                 .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
    888                         "true")
    889                 .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
    890                 .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
    891     }
    892 
    893     /**
    894      * Returns the value of the specified row and column in the Events table, as an integer.
    895      * Throws an exception if the specified row or column doesn't exist or doesn't contain
    896      * an integer (e.g. null entry).
    897      */
    898     private static int getIntFromDatabase(ContentResolver resolver, Uri uri, long rowId,
    899             String columnName) {
    900         String[] projection = { columnName };
    901         String selection = SQL_WHERE_ID;
    902         String[] selectionArgs = { String.valueOf(rowId) };
    903 
    904         Cursor c = resolver.query(uri, projection, selection, selectionArgs, null);
    905         try {
    906             assertEquals(1, c.getCount());
    907             c.moveToFirst();
    908             return c.getInt(0);
    909         } finally {
    910             c.close();
    911         }
    912     }
    913 
    914     @Override
    915     protected void setUp() throws Exception {
    916         super.setUp();
    917         mContentResolver = getInstrumentation().getTargetContext().getContentResolver();
    918     }
    919 
    920     @MediumTest
    921     public void testCalendarCreationAndDeletion() {
    922         String account = "cc1_account";
    923         int seed = 0;
    924 
    925         // Clean up just in case
    926         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
    927         long id = createAndVerifyCalendar(account, seed++, null);
    928 
    929         removeAndVerifyCalendar(account, id);
    930     }
    931 
    932     /**
    933      * Tests whether the default projections work.  We don't need to have any data in
    934      * the calendar, since it's testing the database schema.
    935      */
    936     @MediumTest
    937     public void testDefaultProjections() {
    938         String account = "dproj_account";
    939         int seed = 0;
    940 
    941         // Clean up just in case
    942         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
    943         long id = createAndVerifyCalendar(account, seed++, null);
    944 
    945         Cursor c;
    946         Uri uri;
    947         // Calendars
    948         c = mContentResolver.query(Calendars.CONTENT_URI, null, null, null, null);
    949         c.close();
    950         // Events
    951         c = mContentResolver.query(Events.CONTENT_URI, null, null, null, null);
    952         c.close();
    953         // Instances
    954         uri = Uri.withAppendedPath(Instances.CONTENT_URI, "0/1");
    955         c = mContentResolver.query(uri, null, null, null, null);
    956         c.close();
    957         // Attendees
    958         c = mContentResolver.query(Attendees.CONTENT_URI, null, null, null, null);
    959         c.close();
    960         // Reminders (only REMINDERS_ID currently uses default projection)
    961         uri = ContentUris.withAppendedId(Reminders.CONTENT_URI, 0);
    962         c = mContentResolver.query(uri, null, null, null, null);
    963         c.close();
    964         // CalendarAlerts
    965         c = mContentResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI,
    966                 null, null, null, null);
    967         c.close();
    968         // CalendarCache
    969         c = mContentResolver.query(CalendarContract.CalendarCache.URI,
    970                 null, null, null, null);
    971         c.close();
    972         // CalendarEntity
    973         c = mContentResolver.query(CalendarContract.CalendarEntity.CONTENT_URI,
    974                 null, null, null, null);
    975         c.close();
    976         // EventEntities
    977         c = mContentResolver.query(CalendarContract.EventsEntity.CONTENT_URI,
    978                 null, null, null, null);
    979         c.close();
    980         // EventDays
    981         uri = Uri.withAppendedPath(CalendarContract.EventDays.CONTENT_URI, "1/2");
    982         c = mContentResolver.query(uri, null, null, null, null);
    983         c.close();
    984         // ExtendedProperties
    985         c = mContentResolver.query(CalendarContract.ExtendedProperties.CONTENT_URI,
    986                 null, null, null, null);
    987         c.close();
    988 
    989         removeAndVerifyCalendar(account, id);
    990     }
    991 
    992     /**
    993      * Exercises the EventsEntity class.
    994      */
    995     @MediumTest
    996     public void testEventsEntityQuery() {
    997         String account = "eeq_account";
    998         int seed = 0;
    999 
   1000         // Clean up just in case.
   1001         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1002 
   1003         // Create calendar.
   1004         long calendarId = createAndVerifyCalendar(account, seed++, null);
   1005 
   1006         // Create three events.  We need to make sure SELF_ATTENDEE_STATUS isn't set, because
   1007         // that causes the provider to generate an Attendees entry, and that'll throw off
   1008         // our expected count.
   1009         ContentValues eventValues;
   1010         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1011         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
   1012         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1013         assertTrue(eventId1 >= 0);
   1014 
   1015         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1016         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
   1017         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1018         assertTrue(eventId2 >= 0);
   1019 
   1020         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1021         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
   1022         long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1023         assertTrue(eventId3 >= 0);
   1024 
   1025         /*
   1026          * Add some attendees, reminders, and extended properties.
   1027          */
   1028         Uri uri, syncUri;
   1029 
   1030         syncUri = asSyncAdapter(Reminders.CONTENT_URI, account, CTS_TEST_TYPE);
   1031         ContentValues remValues = new ContentValues();
   1032         remValues.put(Reminders.EVENT_ID, eventId1);
   1033         remValues.put(Reminders.MINUTES, 10);
   1034         remValues.put(Reminders.METHOD, Reminders.METHOD_ALERT);
   1035         mContentResolver.insert(syncUri, remValues);
   1036         remValues.put(Reminders.MINUTES, 20);
   1037         mContentResolver.insert(syncUri, remValues);
   1038 
   1039         syncUri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE);
   1040         ContentValues extended = new ContentValues();
   1041         extended.put(ExtendedProperties.NAME, "foo");
   1042         extended.put(ExtendedProperties.VALUE, "bar");
   1043         extended.put(ExtendedProperties.EVENT_ID, eventId2);
   1044         mContentResolver.insert(syncUri, extended);
   1045         extended.put(ExtendedProperties.EVENT_ID, eventId1);
   1046         mContentResolver.insert(syncUri, extended);
   1047         extended.put(ExtendedProperties.NAME, "foo2");
   1048         extended.put(ExtendedProperties.VALUE, "bar2");
   1049         mContentResolver.insert(syncUri, extended);
   1050 
   1051         syncUri = asSyncAdapter(Attendees.CONTENT_URI, account, CTS_TEST_TYPE);
   1052         ContentValues attendee = new ContentValues();
   1053         attendee.put(Attendees.ATTENDEE_NAME, "Joe");
   1054         attendee.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account));
   1055         attendee.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED);
   1056         attendee.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
   1057         attendee.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_PERFORMER);
   1058         attendee.put(Attendees.EVENT_ID, eventId3);
   1059         mContentResolver.insert(syncUri, attendee);
   1060 
   1061         /*
   1062          * Iterate over all events on our calendar.  Peek at a few values to see if they
   1063          * look reasonable.
   1064          */
   1065         EntityIterator ei = EventsEntity.newEntityIterator(
   1066                 mContentResolver.query(EventsEntity.CONTENT_URI, null, Events.CALENDAR_ID + "=?",
   1067                         new String[] { String.valueOf(calendarId) }, null),
   1068                 mContentResolver);
   1069         int count = 0;
   1070         try {
   1071             while (ei.hasNext()) {
   1072                 Entity entity = ei.next();
   1073                 ContentValues values = entity.getEntityValues();
   1074                 ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
   1075                 long eventId = values.getAsLong(Events._ID);
   1076                 if (eventId == eventId1) {
   1077                     // 2 x reminder, 2 x extended properties
   1078                     assertEquals(4, subvalues.size());
   1079                 } else if (eventId == eventId2) {
   1080                     // Extended properties
   1081                     assertEquals(1, subvalues.size());
   1082                     ContentValues subContentValues = subvalues.get(0).values;
   1083                     String name = subContentValues.getAsString(
   1084                             CalendarContract.ExtendedProperties.NAME);
   1085                     String value = subContentValues.getAsString(
   1086                             CalendarContract.ExtendedProperties.VALUE);
   1087                     assertEquals("foo", name);
   1088                     assertEquals("bar", value);
   1089                 } else if (eventId == eventId3) {
   1090                     // Attendees
   1091                     assertEquals(1, subvalues.size());
   1092                 } else {
   1093                     fail("should not be here");
   1094                 }
   1095                 count++;
   1096             }
   1097             assertEquals(3, count);
   1098         } finally {
   1099             ei.close();
   1100         }
   1101 
   1102         // Confirm that querying for a single event yields a single event.
   1103         ei = EventsEntity.newEntityIterator(
   1104                 mContentResolver.query(EventsEntity.CONTENT_URI, null, SQL_WHERE_ID,
   1105                         new String[] { String.valueOf(eventId3) }, null),
   1106                 mContentResolver);
   1107         try {
   1108             count = 0;
   1109             while (ei.hasNext()) {
   1110                 Entity entity = ei.next();
   1111                 count++;
   1112             }
   1113             assertEquals(1, count);
   1114         } finally {
   1115             ei.close();
   1116         }
   1117 
   1118 
   1119         removeAndVerifyCalendar(account, calendarId);
   1120     }
   1121 
   1122     /**
   1123      * Exercises the CalendarEntity class.
   1124      */
   1125     @MediumTest
   1126     public void testCalendarEntityQuery() {
   1127         String account1 = "ceq1_account";
   1128         String account2 = "ceq2_account";
   1129         String account3 = "ceq3_account";
   1130         int seed = 0;
   1131 
   1132         // Clean up just in case.
   1133         CalendarHelper.deleteCalendarByAccount(mContentResolver, account1);
   1134         CalendarHelper.deleteCalendarByAccount(mContentResolver, account2);
   1135         CalendarHelper.deleteCalendarByAccount(mContentResolver, account3);
   1136 
   1137         // Create calendars.
   1138         long calendarId1 = createAndVerifyCalendar(account1, seed++, null);
   1139         long calendarId2 = createAndVerifyCalendar(account2, seed++, null);
   1140         long calendarId3 = createAndVerifyCalendar(account3, seed++, null);
   1141 
   1142         EntityIterator ei = CalendarEntity.newEntityIterator(
   1143                 mContentResolver.query(CalendarEntity.CONTENT_URI, null,
   1144                         Calendars._ID + "=? OR " + Calendars._ID + "=? OR " + Calendars._ID + "=?",
   1145                         new String[] { String.valueOf(calendarId1), String.valueOf(calendarId2),
   1146                                 String.valueOf(calendarId3) },
   1147                         null));
   1148 
   1149         try {
   1150             int count = 0;
   1151             while (ei.hasNext()) {
   1152                 Entity entity = ei.next();
   1153                 count++;
   1154             }
   1155             assertEquals(3, count);
   1156         } finally {
   1157             ei.close();
   1158         }
   1159 
   1160         removeAndVerifyCalendar(account1, calendarId1);
   1161         removeAndVerifyCalendar(account2, calendarId2);
   1162         removeAndVerifyCalendar(account3, calendarId3);
   1163     }
   1164 
   1165     /**
   1166      * Tests creation and manipulation of Attendees.
   1167      */
   1168     @MediumTest
   1169     public void testAttendees() {
   1170         String account = "att_account";
   1171         int seed = 0;
   1172 
   1173         // Clean up just in case.
   1174         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1175 
   1176         // Create calendar.
   1177         long calendarId = createAndVerifyCalendar(account, seed++, null);
   1178 
   1179         // Create two events, one with a value set for SELF_ATTENDEE_STATUS, one without.
   1180         ContentValues eventValues;
   1181         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1182         eventValues.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE);
   1183         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1184         assertTrue(eventId1 >= 0);
   1185 
   1186         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1187         eventValues.remove(Events.SELF_ATTENDEE_STATUS);
   1188         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1189         assertTrue(eventId2 >= 0);
   1190 
   1191         /*
   1192          * Add some attendees to the first event.
   1193          */
   1194         long attId1 = AttendeeHelper.addAttendee(mContentResolver, eventId1,
   1195                 "Alice",
   1196                 "alice (at) example.com",
   1197                 Attendees.ATTENDEE_STATUS_TENTATIVE,
   1198                 Attendees.RELATIONSHIP_ATTENDEE,
   1199                 Attendees.TYPE_REQUIRED);
   1200         long attId2 = AttendeeHelper.addAttendee(mContentResolver, eventId1,
   1201                 "Betty",
   1202                 "betty (at) example.com",
   1203                 Attendees.ATTENDEE_STATUS_DECLINED,
   1204                 Attendees.RELATIONSHIP_ATTENDEE,
   1205                 Attendees.TYPE_NONE);
   1206         long attId3 = AttendeeHelper.addAttendee(mContentResolver, eventId1,
   1207                 "Carol",
   1208                 "carol (at) example.com",
   1209                 Attendees.ATTENDEE_STATUS_DECLINED,
   1210                 Attendees.RELATIONSHIP_ATTENDEE,
   1211                 Attendees.TYPE_OPTIONAL);
   1212 
   1213         /*
   1214          * Find the event1 "self" attendee entry.
   1215          */
   1216         Cursor cursor = AttendeeHelper.findAttendeesByEmail(mContentResolver, eventId1,
   1217                 CalendarHelper.generateCalendarOwnerEmail(account));
   1218         try {
   1219             assertEquals(1, cursor.getCount());
   1220             //DatabaseUtils.dumpCursor(cursor);
   1221 
   1222             cursor.moveToFirst();
   1223             long id = cursor.getLong(AttendeeHelper.ATTENDEES_ID_INDEX);
   1224 
   1225             /*
   1226              * Update the status field.  The provider should automatically propagate the result.
   1227              */
   1228             ContentValues update = new ContentValues();
   1229             Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, id);
   1230 
   1231             update.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
   1232             int count = mContentResolver.update(uri, update, null, null);
   1233             assertEquals(1, count);
   1234 
   1235             int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId1);
   1236             assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status);
   1237 
   1238         } finally {
   1239             if (cursor != null) {
   1240                 cursor.close();
   1241             }
   1242         }
   1243 
   1244         /*
   1245          * Do a bulk update of all Attendees for this event, changing any Attendee with status
   1246          * "declined" to "invited".
   1247          */
   1248         ContentValues bulkUpdate = new ContentValues();
   1249         bulkUpdate.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_INVITED);
   1250 
   1251         int count = mContentResolver.update(Attendees.CONTENT_URI, bulkUpdate,
   1252                 Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_STATUS + "=?",
   1253                 new String[] {
   1254                     String.valueOf(eventId1), String.valueOf(Attendees.ATTENDEE_STATUS_DECLINED)
   1255                 });
   1256         assertEquals(2, count);
   1257 
   1258         /*
   1259          * Add a new, non-self attendee to the second event.
   1260          */
   1261         long attId4 = AttendeeHelper.addAttendee(mContentResolver, eventId2,
   1262                 "Diana",
   1263                 "diana (at) example.com",
   1264                 Attendees.ATTENDEE_STATUS_ACCEPTED,
   1265                 Attendees.RELATIONSHIP_ATTENDEE,
   1266                 Attendees.TYPE_REQUIRED);
   1267 
   1268         /*
   1269          * Confirm that the selfAttendeeStatus on the second event has the default value.
   1270          */
   1271         int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2);
   1272         assertEquals(Attendees.ATTENDEE_STATUS_NONE, status);
   1273 
   1274         /*
   1275          * Create a new "self" attendee in the second event by updating the email address to
   1276          * match that of the calendar owner.
   1277          */
   1278         ContentValues newSelf = new ContentValues();
   1279         newSelf.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account));
   1280         count = mContentResolver.update(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4),
   1281                 newSelf, null, null);
   1282         assertEquals(1, count);
   1283 
   1284         /*
   1285          * Confirm that the event's selfAttendeeStatus has been updated.
   1286          */
   1287         status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2);
   1288         assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status);
   1289 
   1290         /*
   1291          * TODO:  (these are unexpected usage patterns)
   1292          * - Update an Attendee's status and event_id to move it to a different event, and
   1293          *   confirm that the selfAttendeeStatus in the destination event is updated (rather
   1294          *   than that of the source event).
   1295          * - Create two Attendees with email addresses that match "self" but have different
   1296          *   values for "status".  Delete one and confirm that selfAttendeeStatus is changed
   1297          *   to that of the remaining Attendee.  (There is no defined behavior for
   1298          *   selfAttendeeStatus when there are multiple matching Attendees.)
   1299          */
   1300 
   1301         /*
   1302          * Test deletion, singly by ID and in bulk.
   1303          */
   1304         count = mContentResolver.delete(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4),
   1305                 null, null);
   1306         assertEquals(1, count);
   1307 
   1308         count = mContentResolver.delete(Attendees.CONTENT_URI, Attendees.EVENT_ID + "=?",
   1309                 new String[] { String.valueOf(eventId1) });
   1310         assertEquals(4, count);     // 3 we created + 1 auto-added by the provider
   1311 
   1312         removeAndVerifyCalendar(account, calendarId);
   1313     }
   1314 
   1315     /**
   1316      * Tests creation and manipulation of Reminders.
   1317      */
   1318     @MediumTest
   1319     public void testReminders() {
   1320         String account = "rem_account";
   1321         int seed = 0;
   1322 
   1323         // Clean up just in case.
   1324         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1325 
   1326         // Create calendar.
   1327         long calendarId = createAndVerifyCalendar(account, seed++, null);
   1328 
   1329         // Create events.
   1330         ContentValues eventValues;
   1331         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1332         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1333         assertTrue(eventId1 >= 0);
   1334         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1335         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1336         assertTrue(eventId2 >= 0);
   1337 
   1338         // No reminders, hasAlarm should be zero.
   1339         int hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
   1340         assertEquals(0, hasAlarm);
   1341         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2);
   1342         assertEquals(0, hasAlarm);
   1343 
   1344         /*
   1345          * Add some reminders.
   1346          */
   1347         long remId1 = ReminderHelper.addReminder(mContentResolver, eventId1,
   1348                 10, Reminders.METHOD_DEFAULT);
   1349         long remId2 = ReminderHelper.addReminder(mContentResolver, eventId1,
   1350                 15, Reminders.METHOD_ALERT);
   1351         long remId3 = ReminderHelper.addReminder(mContentResolver, eventId1,
   1352                 20, Reminders.METHOD_SMS);  // SMS isn't allowed for this calendar
   1353 
   1354         // Should have been set to 1 by provider.
   1355         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
   1356         assertEquals(1, hasAlarm);
   1357 
   1358         // Add a reminder to event2.
   1359         ReminderHelper.addReminder(mContentResolver, eventId2,
   1360                 20, Reminders.METHOD_DEFAULT);
   1361         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2);
   1362         assertEquals(1, hasAlarm);
   1363 
   1364 
   1365         /*
   1366          * Check the entries.
   1367          */
   1368         Cursor cursor = ReminderHelper.findRemindersByEventId(mContentResolver, eventId1);
   1369         try {
   1370             assertEquals(3, cursor.getCount());
   1371             //DatabaseUtils.dumpCursor(cursor);
   1372 
   1373             while (cursor.moveToNext()) {
   1374                 int minutes = cursor.getInt(ReminderHelper.REMINDERS_MINUTES_INDEX);
   1375                 int method = cursor.getInt(ReminderHelper.REMINDERS_METHOD_INDEX);
   1376                 switch (minutes) {
   1377                     case 10:
   1378                         assertEquals(Reminders.METHOD_DEFAULT, method);
   1379                         break;
   1380                     case 15:
   1381                         assertEquals(Reminders.METHOD_ALERT, method);
   1382                         break;
   1383                     case 20:
   1384                         assertEquals(Reminders.METHOD_SMS, method);
   1385                         break;
   1386                     default:
   1387                         fail("unexpected minutes " + minutes);
   1388                         break;
   1389                 }
   1390             }
   1391         } finally {
   1392             if (cursor != null) {
   1393                 cursor.close();
   1394             }
   1395         }
   1396 
   1397         /*
   1398          * Use the bulk update feature to change all METHOD_DEFAULT to METHOD_EMAIL.  To make
   1399          * this more interesting we first change remId3 to METHOD_DEFAULT.
   1400          */
   1401         int count;
   1402         ContentValues newValues = new ContentValues();
   1403         newValues.put(Reminders.METHOD, Reminders.METHOD_DEFAULT);
   1404         count = mContentResolver.update(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId3),
   1405                 newValues, null, null);
   1406         assertEquals(1, count);
   1407 
   1408         newValues.put(Reminders.METHOD, Reminders.METHOD_EMAIL);
   1409         count = mContentResolver.update(Reminders.CONTENT_URI, newValues,
   1410                 Reminders.EVENT_ID + "=? AND " + Reminders.METHOD + "=?",
   1411                 new String[] {
   1412                     String.valueOf(eventId1), String.valueOf(Reminders.METHOD_DEFAULT)
   1413                 });
   1414         assertEquals(2, count);
   1415 
   1416         // check it
   1417         int method = ReminderHelper.lookupMethod(mContentResolver, remId3);
   1418         assertEquals(Reminders.METHOD_EMAIL, method);
   1419 
   1420         /*
   1421          * Delete some / all reminders and confirm that hasAlarm tracks it.
   1422          *
   1423          * You can also remove reminders from an event by updating the event_id column, but
   1424          * that's defined as producing undefined behavior, so we don't do it here.
   1425          */
   1426         count = mContentResolver.delete(Reminders.CONTENT_URI,
   1427                 Reminders.EVENT_ID + "=? AND " + Reminders.MINUTES + ">=?",
   1428                 new String[] { String.valueOf(eventId1), "15" });
   1429         assertEquals(2, count);
   1430         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
   1431         assertEquals(1, hasAlarm);
   1432 
   1433         // Delete all reminders from both events.
   1434         count = mContentResolver.delete(Reminders.CONTENT_URI,
   1435                 Reminders.EVENT_ID + "=? OR " + Reminders.EVENT_ID + "=?",
   1436                 new String[] { String.valueOf(eventId1), String.valueOf(eventId2) });
   1437         assertEquals(2, count);
   1438         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1);
   1439         assertEquals(0, hasAlarm);
   1440         hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2);
   1441         assertEquals(0, hasAlarm);
   1442 
   1443         /*
   1444          * Add a couple of reminders and then delete one with the by-ID URI.
   1445          */
   1446         long remId4 = ReminderHelper.addReminder(mContentResolver, eventId1,
   1447                 10, Reminders.METHOD_EMAIL);
   1448         long remId5 = ReminderHelper.addReminder(mContentResolver, eventId1,
   1449                 15, Reminders.METHOD_EMAIL);
   1450         count = mContentResolver.delete(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId4),
   1451                 null, null);
   1452         assertEquals(1, count);
   1453 
   1454         removeAndVerifyCalendar(account, calendarId);
   1455     }
   1456 
   1457     /**
   1458      * A listener for the EVENT_REMINDER broadcast that is expected to be fired by the
   1459      * provider at the reminder time.
   1460      */
   1461     public class MockReminderReceiver extends BroadcastReceiver {
   1462         public boolean received = false;
   1463 
   1464         @Override
   1465         public void onReceive(Context context, Intent intent) {
   1466             final String action = intent.getAction();
   1467             if (action.equals(CalendarContract.ACTION_EVENT_REMINDER)) {
   1468                 received = true;
   1469             }
   1470         }
   1471     }
   1472 
   1473     /**
   1474      * Test that reminders result in the expected broadcast at reminder time.
   1475      */
   1476     public void testRemindersAlarm() throws Exception {
   1477         // Setup: register a mock listener for the broadcast we expect to fire at the
   1478         // reminder time.
   1479         final MockReminderReceiver reminderReceiver = new MockReminderReceiver();
   1480         IntentFilter filter = new IntentFilter(CalendarContract.ACTION_EVENT_REMINDER);
   1481         filter.addDataScheme("content");
   1482         getInstrumentation().getTargetContext().registerReceiver(reminderReceiver, filter);
   1483 
   1484         // Clean up just in case.
   1485         String account = "rem_account";
   1486         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1487 
   1488         // Create calendar.  Use '1' as seed as this sets the VISIBLE field to 1.
   1489         // The calendar must be visible for its notifications to occur.
   1490         long calendarId = createAndVerifyCalendar(account, 1, null);
   1491 
   1492         // Create event for 15 min in the past, with a 10 min reminder, so that it will
   1493         // trigger immediately.
   1494         ContentValues eventValues;
   1495         int seed = 0;
   1496         long now = System.currentTimeMillis();
   1497         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1498         eventValues.put(Events.DTSTART, now - DateUtils.MINUTE_IN_MILLIS * 15);
   1499         eventValues.put(Events.DTEND, now + DateUtils.HOUR_IN_MILLIS);
   1500         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1501         assertTrue(eventId >= 0);
   1502         ReminderHelper.addReminder(mContentResolver, eventId, 10, Reminders.METHOD_ALERT);
   1503 
   1504         // Confirm that the EVENT_REMINDER broadcast was fired by the provider.
   1505         new PollingCheck(POLLING_TIMEOUT) {
   1506             @Override
   1507             protected boolean check() {
   1508                 return reminderReceiver.received;
   1509             }
   1510         }.run();
   1511         assertTrue(reminderReceiver.received);
   1512 
   1513         removeAndVerifyCalendar(account, calendarId);
   1514     }
   1515 
   1516     @MediumTest
   1517     public void testColorWriteRequirements() {
   1518         String account = "colw_account";
   1519         String account2 = "colw2_account";
   1520         int seed = 0;
   1521         Uri uri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE);
   1522         Uri uri2 = asSyncAdapter(Colors.CONTENT_URI, account2, CTS_TEST_TYPE);
   1523 
   1524         // Clean up just in case
   1525         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1526         ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
   1527 
   1528         ContentValues colorValues = new ContentValues();
   1529         // Account name/type must be in the query params, so may be left
   1530         // out here
   1531         colorValues.put(Colors.DATA, "0");
   1532         colorValues.put(Colors.COLOR_KEY, "1");
   1533         colorValues.put(Colors.COLOR_TYPE, 0);
   1534         colorValues.put(Colors.COLOR, 0xff000000);
   1535 
   1536         // Verify only a sync adapter can write to Colors
   1537         try {
   1538             mContentResolver.insert(Colors.CONTENT_URI, colorValues);
   1539             fail("Should not allow non-sync adapter to insert colors");
   1540         } catch (IllegalArgumentException e) {
   1541             // WAI
   1542         }
   1543 
   1544         // Verify everything except DATA is required
   1545         ContentValues testVals = new ContentValues(colorValues);
   1546         for (String key : colorValues.keySet()) {
   1547 
   1548             testVals.remove(key);
   1549             try {
   1550                 Uri colUri = mContentResolver.insert(uri, testVals);
   1551                 if (!TextUtils.equals(key, Colors.DATA)) {
   1552                     // The DATA field is allowed to be empty.
   1553                     fail("Should not allow color creation without " + key);
   1554                 }
   1555                 ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1556             } catch (IllegalArgumentException e) {
   1557                 if (TextUtils.equals(key, Colors.DATA)) {
   1558                     // The DATA field is allowed to be empty.
   1559                     fail("Should allow color creation without " + key);
   1560                 }
   1561             }
   1562             testVals.put(key, colorValues.getAsString(key));
   1563         }
   1564 
   1565         // Verify writing a color works
   1566         Uri col1 = mContentResolver.insert(uri, colorValues);
   1567 
   1568         // Verify adding the same color fails
   1569         try {
   1570             mContentResolver.insert(uri, colorValues);
   1571             fail("Should not allow adding the same color twice");
   1572         } catch (IllegalArgumentException e) {
   1573             // WAI
   1574         }
   1575 
   1576         // Verify specifying a different account than the query params doesn't work
   1577         colorValues.put(Colors.ACCOUNT_NAME, account2);
   1578         try {
   1579             mContentResolver.insert(uri, colorValues);
   1580             fail("Should use the account from the query params, not the values.");
   1581         } catch (IllegalArgumentException e) {
   1582             // WAI
   1583         }
   1584 
   1585         // Verify adding a color to a different account works
   1586         Uri col2 = mContentResolver.insert(uri2, colorValues);
   1587 
   1588         // And a different index on the same account
   1589         colorValues.put(Colors.COLOR_KEY, "2");
   1590         Uri col3 = mContentResolver.insert(uri2, colorValues);
   1591 
   1592         // Verify that all three colors are in the table
   1593         Cursor c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1594         assertEquals(1, c.getCount());
   1595         c.close();
   1596         c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
   1597         assertEquals(2, c.getCount());
   1598         c.close();
   1599 
   1600         // Verify deleting them works
   1601         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1602         ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
   1603 
   1604         c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1605         assertEquals(0, c.getCount());
   1606         c.close();
   1607         c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE);
   1608         assertEquals(0, c.getCount());
   1609         c.close();
   1610     }
   1611 
   1612     /**
   1613      * Tests Colors interaction with the Calendars table.
   1614      */
   1615     @MediumTest
   1616     public void testCalendarColors() {
   1617         String account = "cc_account";
   1618         int seed = 0;
   1619 
   1620         // Clean up just in case
   1621         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1622         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1623 
   1624         // Test inserting a calendar with an invalid color index
   1625         ContentValues cv = CalendarHelper.getNewCalendarValues(account, seed++);
   1626         cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex");
   1627         Uri calSyncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE);
   1628         Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE);
   1629 
   1630         try {
   1631             Uri uri = mContentResolver.insert(calSyncUri, cv);
   1632             fail("Should not allow insertion of invalid color index into Calendars");
   1633         } catch (IllegalArgumentException e) {
   1634             // WAI
   1635         }
   1636 
   1637         // Test updating a calendar with an invalid color index
   1638         long calendarId = createAndVerifyCalendar(account, seed++, null);
   1639         cv.clear();
   1640         cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex2");
   1641         Uri calendarUri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId);
   1642         try {
   1643             mContentResolver.update(calendarUri, cv, null, null);
   1644             fail("Should not allow update of invalid color index into Calendars");
   1645         } catch (IllegalArgumentException e) {
   1646             // WAI
   1647         }
   1648 
   1649         assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE));
   1650 
   1651         // Test that inserting a valid color index works
   1652         cv = CalendarHelper.getNewCalendarValues(account, seed++);
   1653         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]);
   1654 
   1655         Uri uri = mContentResolver.insert(calSyncUri, cv);
   1656         long calendarId2 = ContentUris.parseId(uri);
   1657         assertTrue(calendarId2 >= 0);
   1658         // And updates the calendar's color to the one in the table
   1659         cv.put(Calendars.CALENDAR_COLOR, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]);
   1660         verifyCalendar(account, cv, calendarId2, 2);
   1661 
   1662         // Test that updating a valid color index also updates the color in a
   1663         // calendar
   1664         cv.clear();
   1665         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]);
   1666         mContentResolver.update(calendarUri, cv, null, null);
   1667         Cursor c = mContentResolver.query(calendarUri,
   1668                 new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR },
   1669                 null, null, null);
   1670         try {
   1671             c.moveToFirst();
   1672             String index = c.getString(0);
   1673             int color = c.getInt(1);
   1674             assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]);
   1675             assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]);
   1676         } finally {
   1677             if (c != null) {
   1678                 c.close();
   1679             }
   1680         }
   1681 
   1682         // And clearing it doesn't change the color
   1683         cv.put(Calendars.CALENDAR_COLOR_KEY, (String) null);
   1684         mContentResolver.update(calendarUri, cv, null, null);
   1685         c = mContentResolver.query(calendarUri,
   1686                 new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR },
   1687                 null, null, null);
   1688         try {
   1689             c.moveToFirst();
   1690             String index = c.getString(0);
   1691             int color = c.getInt(1);
   1692             assertEquals(index, null);
   1693             assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0], color);
   1694         } finally {
   1695             if (c != null) {
   1696                 c.close();
   1697             }
   1698         }
   1699 
   1700         // Test that setting a calendar color to an event color fails
   1701         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0]);
   1702         try {
   1703             mContentResolver.update(calendarUri, cv, null, null);
   1704             fail("Should not allow a calendar to use an event color");
   1705         } catch (IllegalArgumentException e) {
   1706             // WAI
   1707         }
   1708 
   1709         // Test that you can't remove a color that is referenced by a calendar
   1710         cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3]);
   1711         mContentResolver.update(calendarUri, cv, null, null);
   1712 
   1713         try {
   1714             mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX,
   1715                     new String[] {
   1716                             account, CTS_TEST_TYPE,
   1717                             ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3]
   1718                     });
   1719             fail("Should not allow deleting referenced color");
   1720         } catch (UnsupportedOperationException e) {
   1721             // WAI
   1722         }
   1723 
   1724         // Clean up
   1725         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1726         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1727     }
   1728 
   1729     /**
   1730      * Tests Colors interaction with the Events table.
   1731      */
   1732     @MediumTest
   1733     public void testEventColors() {
   1734         String account = "ec_account";
   1735         int seed = 0;
   1736 
   1737         // Clean up just in case
   1738         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1739         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1740 
   1741         // Test inserting an event with an invalid color index
   1742         long cal_id = createAndVerifyCalendar(account, seed++, null);
   1743 
   1744         Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE);
   1745 
   1746         ContentValues ev = EventHelper.getNewEventValues(account, seed++, cal_id, false);
   1747         ev.put(Events.EVENT_COLOR_KEY, "badIndex");
   1748 
   1749         try {
   1750             Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev);
   1751             fail("Should not allow insertion of invalid color index into Events");
   1752         } catch (IllegalArgumentException e) {
   1753             // WAI
   1754         }
   1755 
   1756         // Test updating an event with an invalid color index fails
   1757         long event_id = createAndVerifyEvent(account, seed++, cal_id, false, null);
   1758         ev.clear();
   1759         ev.put(Events.EVENT_COLOR_KEY, "badIndex2");
   1760         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, event_id);
   1761         try {
   1762             mContentResolver.update(eventUri, ev, null, null);
   1763             fail("Should not allow update of invalid color index into Events");
   1764         } catch (IllegalArgumentException e) {
   1765             // WAI
   1766         }
   1767 
   1768         assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE));
   1769 
   1770         // Test that inserting a valid color index works
   1771         ev = EventHelper.getNewEventValues(account, seed++, cal_id, false);
   1772         final String defaultColorIndex = ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0];
   1773         ev.put(Events.EVENT_COLOR_KEY, defaultColorIndex);
   1774 
   1775         Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev);
   1776         long eventId2 = ContentUris.parseId(uri);
   1777         assertTrue(eventId2 >= 0);
   1778         // And updates the event's color to the one in the table
   1779         final int expectedColor = ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_0];
   1780         ev.put(Events.EVENT_COLOR, expectedColor);
   1781         verifyEvent(ev, eventId2);
   1782 
   1783         // Test that event iterator has COLOR columns
   1784         final EntityIterator iterator = EventsEntity.newEntityIterator(mContentResolver.query(
   1785                 ContentUris.withAppendedId(EventsEntity.CONTENT_URI, eventId2),
   1786                 null, null, null, null), mContentResolver);
   1787         assertTrue("Empty Iterator", iterator.hasNext());
   1788         final Entity entity = iterator.next();
   1789         final ContentValues values = entity.getEntityValues();
   1790         assertTrue("Missing EVENT_COLOR", values.containsKey(EventsEntity.EVENT_COLOR));
   1791         assertEquals("Wrong EVENT_COLOR",
   1792                 expectedColor,
   1793                 (int) values.getAsInteger(EventsEntity.EVENT_COLOR));
   1794         assertTrue("Missing EVENT_COLOR_KEY", values.containsKey(EventsEntity.EVENT_COLOR_KEY));
   1795         assertEquals("Wrong EVENT_COLOR_KEY",
   1796                 defaultColorIndex,
   1797                 values.getAsString(EventsEntity.EVENT_COLOR_KEY));
   1798         iterator.close();
   1799 
   1800         // Test that updating a valid color index also updates the color in an
   1801         // event
   1802         ev.clear();
   1803         ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]);
   1804         mContentResolver.update(eventUri, ev, null, null);
   1805         Cursor c = mContentResolver.query(eventUri, new String[] {
   1806                 Events.EVENT_COLOR_KEY, Events.EVENT_COLOR
   1807         }, null, null, null);
   1808         try {
   1809             c.moveToFirst();
   1810             String index = c.getString(0);
   1811             int color = c.getInt(1);
   1812             assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]);
   1813             assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1]);
   1814         } finally {
   1815             if (c != null) {
   1816                 c.close();
   1817             }
   1818         }
   1819 
   1820         // And clearing it doesn't change the color
   1821         ev.put(Events.EVENT_COLOR_KEY, (String) null);
   1822         mContentResolver.update(eventUri, ev, null, null);
   1823         c = mContentResolver.query(eventUri, new String[] {
   1824                 Events.EVENT_COLOR_KEY, Events.EVENT_COLOR
   1825         }, null, null, null);
   1826         try {
   1827             c.moveToFirst();
   1828             String index = c.getString(0);
   1829             int color = c.getInt(1);
   1830             assertEquals(index, null);
   1831             assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1], color);
   1832         } finally {
   1833             if (c != null) {
   1834                 c.close();
   1835             }
   1836         }
   1837 
   1838         // Test that setting an event color to a calendar color fails
   1839         ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_2]);
   1840         try {
   1841             mContentResolver.update(eventUri, ev, null, null);
   1842             fail("Should not allow an event to use a calendar color");
   1843         } catch (IllegalArgumentException e) {
   1844             // WAI
   1845         }
   1846 
   1847         // Test that you can't remove a color that is referenced by an event
   1848         ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]);
   1849         mContentResolver.update(eventUri, ev, null, null);
   1850         try {
   1851             mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX,
   1852                     new String[] {
   1853                             account, CTS_TEST_TYPE,
   1854                             ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]
   1855                     });
   1856             fail("Should not allow deleting referenced color");
   1857         } catch (UnsupportedOperationException e) {
   1858             // WAI
   1859         }
   1860 
   1861         // TODO test colors with exceptions
   1862 
   1863         // Clean up
   1864         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1865         ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE);
   1866     }
   1867 
   1868     /**
   1869      * Tests creation and manipulation of ExtendedProperties.
   1870      */
   1871     @MediumTest
   1872     public void testExtendedProperties() {
   1873         String account = "ep_account";
   1874         int seed = 0;
   1875 
   1876         // Clean up just in case.
   1877         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1878 
   1879         // Create calendar.
   1880         long calendarId = createAndVerifyCalendar(account, seed++, null);
   1881 
   1882         // Create events.
   1883         ContentValues eventValues;
   1884         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   1885         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   1886         assertTrue(eventId1 >= 0);
   1887 
   1888         /*
   1889          * Add some extended properties.
   1890          */
   1891         long epId1 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account,
   1892                 eventId1, "first", "Jeffrey");
   1893         long epId2 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account,
   1894                 eventId1, "last", "Lebowski");
   1895         long epId3 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account,
   1896                 eventId1, "title", "Dude");
   1897 
   1898         /*
   1899          * Spot-check a couple of entries.
   1900          */
   1901         Cursor cursor = ExtendedPropertiesHelper.findExtendedPropertiesByEventId(mContentResolver,
   1902                 eventId1);
   1903         try {
   1904             assertEquals(3, cursor.getCount());
   1905             //DatabaseUtils.dumpCursor(cursor);
   1906 
   1907             while (cursor.moveToNext()) {
   1908                 String name =
   1909                     cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_NAME_INDEX);
   1910                 String value =
   1911                     cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_VALUE_INDEX);
   1912 
   1913                 if (name.equals("last")) {
   1914                     assertEquals("Lebowski", value);
   1915                 }
   1916             }
   1917 
   1918             String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1,
   1919                     "title");
   1920             assertEquals("Dude", title);
   1921         } finally {
   1922             if (cursor != null) {
   1923                 cursor.close();
   1924             }
   1925         }
   1926 
   1927         // Update the title.  Must be done as a sync adapter.
   1928         ContentValues newValues = new ContentValues();
   1929         newValues.put(ExtendedProperties.VALUE, "Big");
   1930         Uri uri = ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, epId3);
   1931         uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
   1932         int count = mContentResolver.update(uri, newValues, null, null);
   1933         assertEquals(1, count);
   1934 
   1935         // check it
   1936         String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1,
   1937                 "title");
   1938         assertEquals("Big", title);
   1939 
   1940         removeAndVerifyCalendar(account, calendarId);
   1941     }
   1942 
   1943     private class CalendarEventHelper {
   1944 
   1945       private long mCalendarId;
   1946       private String mAccount;
   1947       private int mSeed;
   1948 
   1949       public CalendarEventHelper(String account, int seed) {
   1950         mAccount = account;
   1951         mSeed = seed;
   1952         ContentValues values = CalendarHelper.getNewCalendarValues(account, seed);
   1953         mCalendarId = createAndVerifyCalendar(account, seed++, values);
   1954       }
   1955 
   1956       public ContentValues addEvent(String timeString, int timeZoneIndex, long duration) {
   1957         long event1Start = timeInMillis(timeString, timeZoneIndex);
   1958         ContentValues eventValues;
   1959         eventValues = EventHelper.getNewEventValues(mAccount, mSeed++, mCalendarId, true);
   1960         eventValues.put(Events.DESCRIPTION, timeString);
   1961         eventValues.put(Events.DTSTART, event1Start);
   1962         eventValues.put(Events.DTEND, event1Start + duration);
   1963         eventValues.put(Events.EVENT_TIMEZONE, TIME_ZONES[timeZoneIndex]);
   1964         long eventId = createAndVerifyEvent(mAccount, mSeed, mCalendarId, true, eventValues);
   1965         assertTrue(eventId >= 0);
   1966         return eventValues;
   1967       }
   1968 
   1969       public long getCalendarId() {
   1970         return mCalendarId;
   1971       }
   1972     }
   1973 
   1974     /**
   1975      * Test query to retrieve instances within a certain time interval.
   1976      */
   1977     public void testWhenByDayQuery() {
   1978       String account = "cser_account";
   1979       int seed = 0;
   1980 
   1981       // Clean up just in case
   1982       CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   1983 
   1984       // Create a calendar
   1985       CalendarEventHelper helper = new CalendarEventHelper(account, seed);
   1986 
   1987       // Add events to the calendar--the first two in the queried range
   1988       List<ContentValues> eventsWithinRange = new ArrayList<ContentValues>();
   1989 
   1990       ContentValues values = helper.addEvent("2009-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS);
   1991       eventsWithinRange.add(values);
   1992 
   1993       values = helper.addEvent("2010-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS);
   1994       eventsWithinRange.add(values);
   1995 
   1996       helper.addEvent("2011-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS);
   1997 
   1998       // Prepare the start time and end time of the range to query
   1999       String startTime = "2009-01-01T00:00:00";
   2000       String endTime = "2011-01-01T00:00:00";
   2001       int julianStart = getJulianDay(startTime, 0);
   2002       int julianEnd = getJulianDay(endTime, 0);
   2003       Uri uri = Uri.withAppendedPath(
   2004           CalendarContract.Instances.CONTENT_BY_DAY_URI, julianStart + "/" + julianEnd);
   2005 
   2006       // Query the range, sorting by event start time
   2007       Cursor c = mContentResolver.query(uri, null, Instances.CALENDAR_ID + "="
   2008               + helper.getCalendarId(), null, Events.DTSTART);
   2009 
   2010       // Assert that two events are returned
   2011       assertEquals(c.getCount(), 2);
   2012 
   2013       Set<String> keySet = new HashSet();
   2014       keySet.add(Events.DESCRIPTION);
   2015       keySet.add(Events.DTSTART);
   2016       keySet.add(Events.DTEND);
   2017       keySet.add(Events.EVENT_TIMEZONE);
   2018 
   2019       // Verify that the contents of those two events match the cursor results
   2020       verifyContentValuesAgainstCursor(eventsWithinRange, keySet, c);
   2021     }
   2022 
   2023     private void verifyContentValuesAgainstCursor(List<ContentValues> cvs,
   2024         Set<String> keys, Cursor cursor) {
   2025       assertEquals(cursor.getCount(), cvs.size());
   2026 
   2027       cursor.moveToFirst();
   2028 
   2029       int i=0;
   2030       do {
   2031         ContentValues cv = cvs.get(i);
   2032         for (String key : keys) {
   2033           assertEquals(cv.get(key).toString(),
   2034                   cursor.getString(cursor.getColumnIndex(key)));
   2035         }
   2036         i++;
   2037       } while (cursor.moveToNext());
   2038 
   2039       cursor.close();
   2040     }
   2041 
   2042     private long timeInMillis(String timeString, int timeZoneIndex) {
   2043       Time startTime = new Time(TIME_ZONES[timeZoneIndex]);
   2044       startTime.parse3339(timeString);
   2045       return startTime.toMillis(false);
   2046     }
   2047 
   2048     private int getJulianDay(String timeString, int timeZoneIndex) {
   2049       Time time = new Time(TIME_ZONES[timeZoneIndex]);
   2050       time.parse3339(timeString);
   2051       return Time.getJulianDay(time.toMillis(false), time.gmtoff);
   2052     }
   2053 
   2054     /**
   2055      * Test instance queries with search parameters.
   2056      */
   2057     @MediumTest
   2058     public void testInstanceSearch() {
   2059         String account = "cser_account";
   2060         int seed = 0;
   2061 
   2062         // Clean up just in case
   2063         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2064 
   2065         // Create a calendar
   2066         ContentValues values = CalendarHelper.getNewCalendarValues(account, seed);
   2067         long calendarId = createAndVerifyCalendar(account, seed++, values);
   2068 
   2069         String testStart = "2009-10-01T08:00:00";
   2070         String timeZone = TIME_ZONES[0];
   2071         Time startTime = new Time(timeZone);
   2072         startTime.parse3339(testStart);
   2073         long startMillis = startTime.toMillis(false);
   2074 
   2075         // Create some events, with different descriptions.  (Could also create a single
   2076         // recurring event and some instance exceptions.)
   2077         ContentValues eventValues;
   2078         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   2079         eventValues.put(Events.DESCRIPTION, "testevent event-one fiddle");
   2080         eventValues.put(Events.DTSTART, startMillis);
   2081         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS);
   2082         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
   2083         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2084         assertTrue(eventId1 >= 0);
   2085 
   2086         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   2087         eventValues.put(Events.DESCRIPTION, "testevent event-two fuzzle");
   2088         eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS);
   2089         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2);
   2090         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
   2091         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2092         assertTrue(eventId2 >= 0);
   2093 
   2094         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   2095         eventValues.put(Events.DESCRIPTION, "testevent event-three fiddle");
   2096         eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS * 2);
   2097         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 3);
   2098         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
   2099         long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2100         assertTrue(eventId3 >= 0);
   2101 
   2102         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   2103         eventValues.put(Events.DESCRIPTION, "nontestevent");
   2104         eventValues.put(Events.DTSTART, startMillis + (long) (DateUtils.HOUR_IN_MILLIS * 1.5f));
   2105         eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2);
   2106         eventValues.put(Events.EVENT_TIMEZONE, timeZone);
   2107         long eventId4 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2108         assertTrue(eventId4 >= 0);
   2109 
   2110         String rangeStart = "2009-10-01T00:00:00";
   2111         String rangeEnd = "2009-10-01T11:59:59";
   2112         String[] projection = new String[] { Instances.BEGIN };
   2113 
   2114         if (false) {
   2115             Cursor instances = getInstances(timeZone, rangeStart, rangeEnd, projection,
   2116                     new long[] { calendarId });
   2117             dumpInstances(instances, timeZone, "all");
   2118             instances.close();
   2119         }
   2120 
   2121         Cursor instances;
   2122         int count;
   2123 
   2124         // Find all matching "testevent".  The search matches on partial strings, so this
   2125         // will also pick up "nontestevent".
   2126         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
   2127                 "testevent", false, projection, new long[] { calendarId });
   2128         count = instances.getCount();
   2129         instances.close();
   2130         assertEquals(4, count);
   2131 
   2132         // Find all matching "fiddle" and "event".  Set the "by day" flag just to be different.
   2133         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
   2134                 "fiddle event", true, projection, new long[] { calendarId });
   2135         count = instances.getCount();
   2136         instances.close();
   2137         assertEquals(2, count);
   2138 
   2139         // Find all matching "fiddle" and "baluchitherium".
   2140         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
   2141                 "baluchitherium fiddle", false, projection, new long[] { calendarId });
   2142         count = instances.getCount();
   2143         instances.close();
   2144         assertEquals(0, count);
   2145 
   2146         // Find all matching "event-two".
   2147         instances = getInstancesSearch(timeZone, rangeStart, rangeEnd,
   2148                 "event-two", false, projection, new long[] { calendarId });
   2149         count = instances.getCount();
   2150         instances.close();
   2151         assertEquals(1, count);
   2152 
   2153         removeAndVerifyCalendar(account, calendarId);
   2154     }
   2155 
   2156     @MediumTest
   2157     public void testCalendarUpdateAsApp() {
   2158         String account = "cu1_account";
   2159         int seed = 0;
   2160 
   2161         // Clean up just in case
   2162         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2163 
   2164         // Create a calendar
   2165         ContentValues values = CalendarHelper.getNewCalendarValues(account, seed);
   2166         long id = createAndVerifyCalendar(account, seed++, values);
   2167 
   2168         Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
   2169 
   2170         // Update the calendar using the direct Uri
   2171         ContentValues updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal(
   2172                 values, seed++);
   2173         assertEquals(1, mContentResolver.update(uri, updateValues, null, null));
   2174 
   2175         verifyCalendar(account, values, id, 1);
   2176 
   2177         // Update the calendar using selection + args
   2178         String selection = Calendars._ID + "=?";
   2179         String[] selectionArgs = new String[] { Long.toString(id) };
   2180 
   2181         updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal(values, seed++);
   2182 
   2183         assertEquals(1, mContentResolver.update(
   2184                 Calendars.CONTENT_URI, updateValues, selection, selectionArgs));
   2185 
   2186         verifyCalendar(account, values, id, 1);
   2187 
   2188         removeAndVerifyCalendar(account, id);
   2189     }
   2190 
   2191     // TODO test calendar updates as sync adapter
   2192 
   2193     /**
   2194      * Test access to the "syncstate" table.
   2195      */
   2196     @MediumTest
   2197     public void testSyncState() {
   2198         String account = "ss_account";
   2199         int seed = 0;
   2200 
   2201         // Clean up just in case
   2202         SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true);
   2203 
   2204         // Create a new sync state entry
   2205         ContentValues values = SyncStateHelper.getNewSyncStateValues(account);
   2206         long id = createAndVerifySyncState(account, values);
   2207 
   2208         // Look it up with the by-ID URI
   2209         Cursor c = SyncStateHelper.getSyncStateById(mContentResolver, id);
   2210         assertNotNull(c);
   2211         assertEquals(1, c.getCount());
   2212         c.close();
   2213 
   2214         // Try to remove it as non-sync-adapter; expected to fail.
   2215         boolean failed;
   2216         try {
   2217             SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, false);
   2218             failed = false;
   2219         } catch (IllegalArgumentException iae) {
   2220             failed = true;
   2221         }
   2222         assertTrue("deletion of sync state by app was allowed", failed);
   2223 
   2224         // Remove it and verify that it's gone
   2225         removeAndVerifySyncState(account);
   2226     }
   2227 
   2228 
   2229     private void verifyEvent(ContentValues values, long eventId) {
   2230         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
   2231         // Verify
   2232         Cursor c = mContentResolver
   2233                 .query(eventUri, EventHelper.EVENTS_PROJECTION, null, null, null);
   2234         assertEquals(1, c.getCount());
   2235         assertTrue(c.moveToFirst());
   2236         assertEquals(eventId, c.getLong(0));
   2237         for (String key : values.keySet()) {
   2238             int index = c.getColumnIndex(key);
   2239             assertEquals(key, values.getAsString(key), c.getString(index));
   2240         }
   2241         c.close();
   2242     }
   2243 
   2244     @MediumTest
   2245     public void testEventCreationAndDeletion() {
   2246         String account = "ec1_account";
   2247         int seed = 0;
   2248 
   2249         // Clean up just in case
   2250         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2251 
   2252         // Create calendar and event
   2253         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2254 
   2255         ContentValues eventValues = EventHelper
   2256                 .getNewEventValues(account, seed++, calendarId, true);
   2257         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2258 
   2259         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
   2260 
   2261         removeAndVerifyEvent(eventUri, eventValues, account);
   2262 
   2263         // Attempt to create an event without a calendar ID.
   2264         ContentValues badValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   2265         badValues.remove(Events.CALENDAR_ID);
   2266         try {
   2267             createAndVerifyEvent(account, seed, calendarId, true, badValues);
   2268             fail("was allowed to create an event without CALENDAR_ID");
   2269         } catch (IllegalArgumentException iae) {
   2270             // expected
   2271         }
   2272 
   2273         // Validation may be relaxed for content providers, so test missing timezone as app.
   2274         badValues = EventHelper.getNewEventValues(account, seed++, calendarId, false);
   2275         badValues.remove(Events.EVENT_TIMEZONE);
   2276         try {
   2277             createAndVerifyEvent(account, seed, calendarId, false, badValues);
   2278             fail("was allowed to create an event without EVENT_TIMEZONE");
   2279         } catch (IllegalArgumentException iae) {
   2280             // expected
   2281         }
   2282 
   2283         removeAndVerifyCalendar(account, calendarId);
   2284     }
   2285 
   2286     @MediumTest
   2287     public void testEventUpdateAsApp() {
   2288         String account = "em1_account";
   2289         int seed = 0;
   2290 
   2291         // Clean up just in case
   2292         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2293 
   2294         // Create calendar
   2295         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2296 
   2297         // Create event as sync adapter
   2298         ContentValues eventValues = EventHelper
   2299                 .getNewEventValues(account, seed++, calendarId, true);
   2300         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2301 
   2302         // Update event as app
   2303         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
   2304 
   2305         ContentValues updateValues = EventHelper.getUpdateEventValuesWithOriginal(eventValues,
   2306                 seed++, false);
   2307         assertEquals(1, mContentResolver.update(eventUri, updateValues, null, null));
   2308         updateValues.put(Events.DIRTY, 1);      // provider should have marked as dirty
   2309         verifyEvent(updateValues, eventId);
   2310 
   2311         // Try nulling out a required value.
   2312         ContentValues badValues = new ContentValues(updateValues);
   2313         badValues.putNull(Events.EVENT_TIMEZONE);
   2314         badValues.remove(Events.DIRTY);
   2315         try {
   2316             mContentResolver.update(eventUri, badValues, null, null);
   2317             fail("was allowed to null out EVENT_TIMEZONE");
   2318         } catch (IllegalArgumentException iae) {
   2319             // good
   2320         }
   2321 
   2322         removeAndVerifyEvent(eventUri, eventValues, account);
   2323 
   2324         // delete the calendar
   2325         removeAndVerifyCalendar(account, calendarId);
   2326     }
   2327 
   2328     /**
   2329      * Tests update of multiple events with a single update call.
   2330      */
   2331     @MediumTest
   2332     public void testBulkUpdate() {
   2333         String account = "bup_account";
   2334         int seed = 0;
   2335 
   2336         // Clean up just in case
   2337         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2338 
   2339         // Create calendar
   2340         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2341         String calendarIdStr = String.valueOf(calendarId);
   2342 
   2343         // Create events
   2344         ContentValues eventValues;
   2345         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   2346         long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2347 
   2348         eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true);
   2349         long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2350 
   2351         // Update the "description" field in all events in this calendar.
   2352         String newDescription = "bulk edit";
   2353         ContentValues updateValues = new ContentValues();
   2354         updateValues.put(Events.DESCRIPTION, newDescription);
   2355 
   2356         // Must be sync adapter to do a bulk update.
   2357         Uri uri = asSyncAdapter(Events.CONTENT_URI, account, CTS_TEST_TYPE);
   2358         int count = mContentResolver.update(uri, updateValues, SQL_WHERE_CALENDAR_ID,
   2359                 new String[] { calendarIdStr });
   2360 
   2361         // Check to see if the changes went through.
   2362         Uri eventUri = Events.CONTENT_URI;
   2363         Cursor c = mContentResolver.query(eventUri, new String[] { Events.DESCRIPTION },
   2364                 SQL_WHERE_CALENDAR_ID, new String[] { calendarIdStr }, null);
   2365         assertEquals(2, c.getCount());
   2366         while (c.moveToNext()) {
   2367             assertEquals(newDescription, c.getString(0));
   2368         }
   2369         c.close();
   2370 
   2371         // delete the calendar
   2372         removeAndVerifyCalendar(account, calendarId);
   2373     }
   2374 
   2375     /**
   2376      * Tests the content provider's enforcement of restrictions on who is allowed to modify
   2377      * specific columns in a Calendar.
   2378      * <p>
   2379      * This attempts to create a new row in the Calendar table, specifying one restricted
   2380      * column at a time.
   2381      */
   2382     @MediumTest
   2383     public void testSyncOnlyInsertEnforcement() {
   2384         // These operations should not succeed, so there should be nothing to clean up after.
   2385         // TODO: this should be a new event augmented with an illegal column, not a single
   2386         //       column.  Otherwise we might be tripping over a "DTSTART must exist" test.
   2387         ContentValues vals = new ContentValues();
   2388         for (int i = 0; i < Calendars.SYNC_WRITABLE_COLUMNS.length; i++) {
   2389             boolean threw = false;
   2390             try {
   2391                 vals.clear();
   2392                 vals.put(Calendars.SYNC_WRITABLE_COLUMNS[i], "1");
   2393                 mContentResolver.insert(Calendars.CONTENT_URI, vals);
   2394             } catch (IllegalArgumentException e) {
   2395                 threw = true;
   2396             }
   2397             assertTrue("Only sync adapter should be allowed to insert "
   2398                     + Calendars.SYNC_WRITABLE_COLUMNS[i], threw);
   2399         }
   2400     }
   2401 
   2402     /**
   2403      * Tests creation of a recurring event.
   2404      * <p>
   2405      * This (and the other recurrence tests) uses dates well in the past to reduce the likelihood
   2406      * of encountering non-test recurring events.  (Ideally we would select events associated
   2407      * with a specific calendar.)  With dates well in the past, it's also important to have a
   2408      * fixed maximum count or end date; otherwise, if the metadata min/max instance values are
   2409      * large enough, the recurrence recalculation processor could get triggered on an insert or
   2410      * update and bump up against the 2000-instance limit.
   2411      *
   2412      * TODO: need some allDay tests
   2413      */
   2414     @MediumTest
   2415     public void testRecurrence() {
   2416         String account = "re_account";
   2417         int seed = 0;
   2418 
   2419         // Clean up just in case
   2420         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2421 
   2422         // Create calendar
   2423         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2424 
   2425         // Create recurring event
   2426         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
   2427                 calendarId, true, "2003-08-05T09:00:00", "PT1H",
   2428                 "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU");
   2429         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2430         //Log.d(TAG, "+++ basic recurrence eventId is " + eventId);
   2431 
   2432         // Check to see if we have the expected number of instances
   2433         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
   2434         int instanceCount = getInstanceCount(timeZone, "2003-08-05T00:00:00",
   2435                 "2003-08-31T11:59:59", new long[] { calendarId });
   2436         if (false) {
   2437             Cursor instances = getInstances(timeZone, "2003-08-05T00:00:00", "2003-08-31T11:59:59",
   2438                     new String[] { Instances.BEGIN }, new long[] { calendarId });
   2439             dumpInstances(instances, timeZone, "initial");
   2440             instances.close();
   2441         }
   2442         assertEquals("recurrence instance count", 4, instanceCount);
   2443 
   2444         // delete the calendar
   2445         removeAndVerifyCalendar(account, calendarId);
   2446     }
   2447 
   2448     /**
   2449      * Tests conversion of a regular event to a recurring event.
   2450      */
   2451     @MediumTest
   2452     public void testConversionToRecurring() {
   2453         String account = "reconv_account";
   2454         int seed = 0;
   2455 
   2456         // Clean up just in case
   2457         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2458 
   2459         // Create calendar and event
   2460         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2461 
   2462         ContentValues eventValues = EventHelper
   2463                 .getNewEventValues(account, seed++, calendarId, true);
   2464         long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
   2465 
   2466         long dtstart = eventValues.getAsLong(Events.DTSTART);
   2467         long dtend = eventValues.getAsLong(Events.DTEND);
   2468         long durationSecs = (dtend - dtstart) / 1000;
   2469 
   2470         ContentValues updateValues = new ContentValues();
   2471         updateValues.put(Events.RRULE, "FREQ=WEEKLY");   // recurs forever
   2472         updateValues.put(Events.DURATION, "P" + durationSecs + "S");
   2473         updateValues.putNull(Events.DTEND);
   2474 
   2475         // Issue update; do it as app instead of sync adapter to exercise that path.
   2476         updateAndVerifyEvent(account, calendarId, eventId, false, updateValues);
   2477 
   2478         // Make sure LAST_DATE got nulled out by our infinitely repeating sequence.
   2479         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
   2480         Cursor c = mContentResolver.query(eventUri, new String[] { Events.LAST_DATE },
   2481                 null, null, null);
   2482         assertEquals(1, c.getCount());
   2483         assertTrue(c.moveToFirst());
   2484         assertNull(c.getString(0));
   2485         c.close();
   2486 
   2487         removeAndVerifyCalendar(account, calendarId);
   2488     }
   2489 
   2490     /**
   2491      * Tests creation of a recurring event with single-instance exceptions.
   2492      */
   2493     @MediumTest
   2494     public void testSingleRecurrenceExceptions() {
   2495         String account = "rex_account";
   2496         int seed = 0;
   2497 
   2498         // Clean up just in case
   2499         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2500 
   2501         // Create calendar
   2502         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2503 
   2504         // Create recurring event.
   2505         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
   2506                 calendarId, true, "1999-03-28T09:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=100");
   2507         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
   2508 
   2509         // Add some attendees and reminders.
   2510         addAttendees(account, eventId, seed);
   2511         addReminders(account, eventId, seed);
   2512 
   2513         // Select a period that gives us 5 instances.  We don't want this to straddle a DST
   2514         // transition, because we expect the startMinute field to be the same for all
   2515         // instances, and it's stored as minutes since midnight in the device's time zone.
   2516         // Things won't be consistent if the event and the device have different ideas about DST.
   2517         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
   2518         String testStart = "1999-04-18T00:00:00";
   2519         String testEnd = "1999-05-16T23:59:59";
   2520         String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.END_MINUTE };
   2521 
   2522         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
   2523                 new long[] { calendarId });
   2524         if (DEBUG_RECURRENCE) {
   2525             dumpInstances(instances, timeZone, "initial");
   2526         }
   2527 
   2528         assertEquals("initial recurrence instance count", 5, instances.getCount());
   2529 
   2530         /*
   2531          * Advance the start time of a few instances, and verify.
   2532          */
   2533 
   2534         // Leave first instance alone.
   2535         instances.moveToPosition(1);
   2536 
   2537         long startMillis;
   2538         ContentValues excepValues;
   2539 
   2540         // Advance the start time of the 2nd instance.
   2541         startMillis = instances.getLong(0);
   2542         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2543         excepValues.put(Events.DTSTART, startMillis + 3600*1000);
   2544         long excepEventId2 = createAndVerifyException(account, eventId, excepValues, true);
   2545         instances.moveToNext();
   2546 
   2547         // Advance the start time of the 3rd instance.
   2548         startMillis = instances.getLong(0);
   2549         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2550         excepValues.put(Events.DTSTART, startMillis + 3600*1000*2);
   2551         long excepEventId3 = createAndVerifyException(account, eventId, excepValues, true);
   2552         instances.moveToNext();
   2553 
   2554         // Cancel the 4th instance.
   2555         startMillis = instances.getLong(0);
   2556         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2557         excepValues.put(Events.STATUS, Events.STATUS_CANCELED);
   2558         long excepEventId4 = createAndVerifyException(account, eventId, excepValues, true);
   2559         instances.moveToNext();
   2560 
   2561         // TODO: try to modify a non-existent instance.
   2562 
   2563         instances.close();
   2564 
   2565         // TODO: compare Reminders, Attendees, ExtendedProperties on one of the exception events
   2566 
   2567         // Re-query the instances and figure out if they look right.
   2568         instances = getInstances(timeZone, testStart, testEnd, projection,
   2569                 new long[] { calendarId });
   2570         if (DEBUG_RECURRENCE) {
   2571             dumpInstances(instances, timeZone, "with DTSTART exceptions");
   2572         }
   2573         assertEquals("exceptional recurrence instance count", 4, instances.getCount());
   2574 
   2575         long prevMinute = -1;
   2576         while (instances.moveToNext()) {
   2577             // expect the start times for each entry to be different from the previous entry
   2578             long startMinute = instances.getLong(1);
   2579             assertTrue("instance start times are different", startMinute != prevMinute);
   2580 
   2581             prevMinute = startMinute;
   2582         }
   2583         instances.close();
   2584 
   2585 
   2586         // Delete all of our exceptions, and verify.
   2587         int deleteCount = 0;
   2588         deleteCount += deleteException(account, eventId, excepEventId2);
   2589         deleteCount += deleteException(account, eventId, excepEventId3);
   2590         deleteCount += deleteException(account, eventId, excepEventId4);
   2591         assertEquals("events deleted", 3, deleteCount);
   2592 
   2593         // Re-query the instances and figure out if they look right.
   2594         instances = getInstances(timeZone, testStart, testEnd, projection,
   2595                 new long[] { calendarId });
   2596         if (DEBUG_RECURRENCE) {
   2597             dumpInstances(instances, timeZone, "post exception deletion");
   2598         }
   2599         assertEquals("post-exception deletion instance count", 5, instances.getCount());
   2600 
   2601         prevMinute = -1;
   2602         while (instances.moveToNext()) {
   2603             // expect the start times for each entry to be the same
   2604             long startMinute = instances.getLong(1);
   2605             if (prevMinute != -1) {
   2606                 assertEquals("instance start times are the same", startMinute, prevMinute);
   2607             }
   2608             prevMinute = startMinute;
   2609         }
   2610         instances.close();
   2611 
   2612         /*
   2613          * Repeat the test, this time modifying DURATION.
   2614          */
   2615 
   2616         instances = getInstances(timeZone, testStart, testEnd, projection,
   2617                 new long[] { calendarId });
   2618         if (DEBUG_RECURRENCE) {
   2619             dumpInstances(instances, timeZone, "initial");
   2620         }
   2621 
   2622         assertEquals("initial recurrence instance count", 5, instances.getCount());
   2623 
   2624         // Leave first instance alone.
   2625         instances.moveToPosition(1);
   2626 
   2627         // Advance the end time of the 2nd instance.
   2628         startMillis = instances.getLong(0);
   2629         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2630         excepValues.put(Events.DURATION, "P" + 3600*2 + "S");
   2631         excepEventId2 = createAndVerifyException(account, eventId, excepValues, true);
   2632         instances.moveToNext();
   2633 
   2634         // Advance the end time of the 3rd instance, and change the self-attendee status.
   2635         startMillis = instances.getLong(0);
   2636         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2637         excepValues.put(Events.DURATION, "P" + 3600*3 + "S");
   2638         excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED);
   2639         excepEventId3 = createAndVerifyException(account, eventId, excepValues, true);
   2640         instances.moveToNext();
   2641 
   2642         // Advance the start time of the 4th instance, which will also advance the end time.
   2643         startMillis = instances.getLong(0);
   2644         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2645         excepValues.put(Events.DTSTART, startMillis + 3600*1000);
   2646         excepEventId4 = createAndVerifyException(account, eventId, excepValues, true);
   2647         instances.moveToNext();
   2648 
   2649         instances.close();
   2650 
   2651         // TODO: make sure the selfAttendeeStatus change took
   2652 
   2653         // Re-query the instances and figure out if they look right.
   2654         instances = getInstances(timeZone, testStart, testEnd, projection,
   2655                 new long[] { calendarId });
   2656         if (DEBUG_RECURRENCE) {
   2657             dumpInstances(instances, timeZone, "with DURATION exceptions");
   2658         }
   2659         assertEquals("exceptional recurrence instance count", 5, instances.getCount());
   2660 
   2661         prevMinute = -1;
   2662         while (instances.moveToNext()) {
   2663             // expect the start times for each entry to be different from the previous entry
   2664             long endMinute = instances.getLong(2);
   2665             assertTrue("instance end times are different", endMinute != prevMinute);
   2666 
   2667             prevMinute = endMinute;
   2668         }
   2669         instances.close();
   2670 
   2671         // delete the calendar
   2672         removeAndVerifyCalendar(account, calendarId);
   2673     }
   2674 
   2675     /**
   2676      * Tests creation of a simple recurrence exception when not pretending to be the sync
   2677      * adapter.  One significant consequence is that we don't set the _sync_id field in the
   2678      * events, which affects how the provider correlates recurrences and exceptions.
   2679      */
   2680     @MediumTest
   2681     public void testNonAdapterRecurrenceExceptions() {
   2682         String account = "rena_account";
   2683         int seed = 0;
   2684 
   2685         // Clean up just in case
   2686         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2687 
   2688         // Create calendar
   2689         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2690 
   2691         // Generate recurring event, with "asSyncAdapter" set to false.
   2692         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
   2693                 calendarId, false, "1991-02-03T12:00:00", "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10");
   2694 
   2695         // Select a period that gives us 3 instances.
   2696         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
   2697         String testStart = "1991-02-03T00:00:00";
   2698         String testEnd = "1991-02-05T23:59:59";
   2699         String[] projection = { Instances.BEGIN, Instances.START_MINUTE };
   2700 
   2701         // Expand the bounds of the instances table so we expand future events as they are added.
   2702         expandInstanceRange(account, calendarId, testStart, testEnd, timeZone);
   2703 
   2704         // Create the event in the database.
   2705         long eventId = createAndVerifyEvent(account, seed++, calendarId, false, eventValues);
   2706         assertTrue(eventId >= 0);
   2707 
   2708         // Add some attendees.
   2709         addAttendees(account, eventId, seed);
   2710 
   2711         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
   2712                 new long[] { calendarId });
   2713         if (DEBUG_RECURRENCE) {
   2714             dumpInstances(instances, timeZone, "initial");
   2715         }
   2716         assertEquals("initial recurrence instance count", 3, instances.getCount());
   2717 
   2718         /*
   2719          * Alter the attendee status of the second event.  This should cause the instances to
   2720          * be updated, replacing the previous 2nd instance with the exception instance.  If the
   2721          * code is broken we'll see four instances (because the original instance didn't get
   2722          * removed) or one instance (because the code correctly deleted all related events but
   2723          * couldn't correlate the exception with its original recurrence).
   2724          */
   2725 
   2726         // Leave first instance alone.
   2727         instances.moveToPosition(1);
   2728 
   2729         long startMillis;
   2730         ContentValues excepValues;
   2731 
   2732         // Advance the start time of the 2nd instance.
   2733         startMillis = instances.getLong(0);
   2734         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2735         excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED);
   2736         long excepEventId2 = createAndVerifyException(account, eventId, excepValues, false);
   2737         instances.moveToNext();
   2738 
   2739         instances.close();
   2740 
   2741         // Re-query the instances and figure out if they look right.
   2742         instances = getInstances(timeZone, testStart, testEnd, projection,
   2743                 new long[] { calendarId });
   2744         if (DEBUG_RECURRENCE) {
   2745             dumpInstances(instances, timeZone, "with exceptions");
   2746         }
   2747 
   2748         // TODO: this test currently fails due to limitations in the provider
   2749         //assertEquals("exceptional recurrence instance count", 3, instances.getCount());
   2750 
   2751         instances.close();
   2752 
   2753         // delete the calendar
   2754         removeAndVerifyCalendar(account, calendarId);
   2755     }
   2756 
   2757     /**
   2758      * Tests insertion of event exceptions before and after a recurring event is created.
   2759      * <p>
   2760      * The server may send exceptions down before the event they refer to, so the provider
   2761      * fills in the originalId of previously-existing exceptions when a recurring event is
   2762      * inserted.  Make sure that works.
   2763      * <p>
   2764      * The _sync_id column is only unique with a given calendar.  We create events with
   2765      * identical originalSyncId values in two different calendars to verify that the provider
   2766      * doesn't update unrelated events.
   2767      * <p>
   2768      * We can't use the /exception URI, because that only works if the events are created
   2769      * in order.
   2770      */
   2771     @MediumTest
   2772     public void testOutOfOrderRecurrenceExceptions() {
   2773         String account1 = "roid1_account";
   2774         String account2 = "roid2_account";
   2775         String startWhen = "1987-08-09T12:00:00";
   2776         int seed = 0;
   2777 
   2778         // Clean up just in case
   2779         CalendarHelper.deleteCalendarByAccount(mContentResolver, account1);
   2780         CalendarHelper.deleteCalendarByAccount(mContentResolver, account2);
   2781 
   2782         // Create calendars
   2783         long calendarId1 = createAndVerifyCalendar(account1, seed++, null);
   2784         long calendarId2 = createAndVerifyCalendar(account2, seed++, null);
   2785 
   2786 
   2787         // Generate base event.
   2788         ContentValues recurEventValues = EventHelper.getNewRecurringEventValues(account1, seed++,
   2789                 calendarId1, true, startWhen, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10");
   2790 
   2791         // Select a period that gives us 3 instances.
   2792         String timeZone = recurEventValues.getAsString(Events.EVENT_TIMEZONE);
   2793         String testStart = "1987-08-09T00:00:00";
   2794         String testEnd = "1987-08-11T23:59:59";
   2795         String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.EVENT_ID };
   2796 
   2797         /*
   2798          * We're interested in exploring what the instance expansion code does with the events
   2799          * as they arrive.  It won't do anything at event-creation time unless the instance
   2800          * range already covers the interesting set of dates, so we need to create and remove
   2801          * an instance in the same time frame beforehand.
   2802          */
   2803         expandInstanceRange(account1, calendarId1, testStart, testEnd, timeZone);
   2804 
   2805         /*
   2806          * Instances table should be expanded.  Do the test.
   2807          */
   2808 
   2809         final String MAGIC_SYNC_ID = "MagicSyncId";
   2810         recurEventValues.put(Events._SYNC_ID, MAGIC_SYNC_ID);
   2811 
   2812         // Generate exceptions from base, removing the generated _sync_id and setting the
   2813         // base event's _sync_id as originalSyncId.
   2814         ContentValues beforeExcepValues, afterExcepValues, unrelatedExcepValues;
   2815         beforeExcepValues = new ContentValues(recurEventValues);
   2816         afterExcepValues = new ContentValues(recurEventValues);
   2817         unrelatedExcepValues = new ContentValues(recurEventValues);
   2818         beforeExcepValues.remove(Events._SYNC_ID);
   2819         afterExcepValues.remove(Events._SYNC_ID);
   2820         unrelatedExcepValues.remove(Events._SYNC_ID);
   2821         beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
   2822         afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
   2823         unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
   2824 
   2825         // Disassociate the "unrelated" exception by moving it to the other calendar.
   2826         unrelatedExcepValues.put(Events.CALENDAR_ID, calendarId2);
   2827 
   2828         // We shift the start time by half an hour, and use the same _sync_id.
   2829         final long ONE_DAY_MILLIS = 24 * 60 * 60 * 1000;
   2830         final long ONE_HOUR_MILLIS  = 60 * 60 * 1000;
   2831         final long HALF_HOUR_MILLIS  = 30 * 60 * 1000;
   2832         long dtstartMillis = recurEventValues.getAsLong(Events.DTSTART) + ONE_DAY_MILLIS;
   2833         beforeExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis);
   2834         beforeExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS);
   2835         beforeExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS);
   2836         beforeExcepValues.remove(Events.DURATION);
   2837         beforeExcepValues.remove(Events.RRULE);
   2838         beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
   2839         dtstartMillis += ONE_DAY_MILLIS;
   2840         afterExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis);
   2841         afterExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS);
   2842         afterExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS);
   2843         afterExcepValues.remove(Events.DURATION);
   2844         afterExcepValues.remove(Events.RRULE);
   2845         afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
   2846         dtstartMillis += ONE_DAY_MILLIS;
   2847         unrelatedExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis);
   2848         unrelatedExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS);
   2849         unrelatedExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS);
   2850         unrelatedExcepValues.remove(Events.DURATION);
   2851         unrelatedExcepValues.remove(Events.RRULE);
   2852         unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID);
   2853 
   2854 
   2855         // Create "before" and "unrelated" exceptions.
   2856         long beforeEventId = createAndVerifyEvent(account1, seed, calendarId1, true,
   2857                 beforeExcepValues);
   2858         assertTrue(beforeEventId >= 0);
   2859         long unrelatedEventId = createAndVerifyEvent(account2, seed, calendarId2, true,
   2860                 unrelatedExcepValues);
   2861         assertTrue(unrelatedEventId >= 0);
   2862 
   2863         // Create recurring event.
   2864         long recurEventId = createAndVerifyEvent(account1, seed, calendarId1, true,
   2865                 recurEventValues);
   2866         assertTrue(recurEventId >= 0);
   2867 
   2868         // Create "after" exception.
   2869         long afterEventId = createAndVerifyEvent(account1, seed, calendarId1, true,
   2870                 afterExcepValues);
   2871         assertTrue(afterEventId >= 0);
   2872 
   2873         if (Log.isLoggable(TAG, Log.DEBUG)) {
   2874             Log.d(TAG, "before=" + beforeEventId + ", unrel=" + unrelatedEventId +
   2875                     ", recur=" + recurEventId + ", after=" + afterEventId);
   2876         }
   2877 
   2878         // Check to see how many instances we get.  If the recurrence and the exception don't
   2879         // get paired up correctly, we'll see too many instances.
   2880         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
   2881                 new long[] { calendarId1, calendarId2 });
   2882         if (DEBUG_RECURRENCE) {
   2883             dumpInstances(instances, timeZone, "with exception");
   2884         }
   2885 
   2886         assertEquals("initial recurrence instance count", 3, instances.getCount());
   2887 
   2888         instances.close();
   2889 
   2890 
   2891         /*
   2892          * Now we want to verify that:
   2893          * - "before" and "after" have an originalId equal to our recurEventId
   2894          * - "unrelated" has no originalId
   2895          */
   2896         Cursor c = null;
   2897         try {
   2898             final String[] PROJECTION = new String[] { Events.ORIGINAL_ID };
   2899             Uri eventUri;
   2900             Long originalId;
   2901 
   2902             eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, beforeEventId);
   2903             c = mContentResolver.query(eventUri, PROJECTION, null, null, null);
   2904             assertEquals(1, c.getCount());
   2905             c.moveToNext();
   2906             originalId = c.getLong(0);
   2907             assertNotNull(originalId);
   2908             assertEquals(recurEventId, (long) originalId);
   2909             c.close();
   2910 
   2911             eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, afterEventId);
   2912             c = mContentResolver.query(eventUri, PROJECTION, null, null, null);
   2913             assertEquals(1, c.getCount());
   2914             c.moveToNext();
   2915             originalId = c.getLong(0);
   2916             assertNotNull(originalId);
   2917             assertEquals(recurEventId, (long) originalId);
   2918             c.close();
   2919 
   2920             eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, unrelatedEventId);
   2921             c = mContentResolver.query(eventUri, PROJECTION, null, null, null);
   2922             assertEquals(1, c.getCount());
   2923             c.moveToNext();
   2924             assertNull(c.getString(0));
   2925             c.close();
   2926 
   2927             c = null;
   2928         } finally {
   2929             if (c != null) {
   2930                 c.close();
   2931             }
   2932         }
   2933 
   2934         // delete the calendars
   2935         removeAndVerifyCalendar(account1, calendarId1);
   2936         removeAndVerifyCalendar(account2, calendarId2);
   2937     }
   2938 
   2939     /**
   2940      * Tests exceptions that modify all future instances of a recurring event.
   2941      */
   2942     @MediumTest
   2943     public void testForwardRecurrenceExceptions() {
   2944         String account = "refx_account";
   2945         int seed = 0;
   2946 
   2947         // Clean up just in case
   2948         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   2949 
   2950         // Create calendar
   2951         long calendarId = createAndVerifyCalendar(account, seed++, null);
   2952 
   2953         // Create recurring event
   2954         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
   2955                 calendarId, true, "1999-01-01T06:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=10");
   2956         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
   2957 
   2958         // Add some attendees and reminders.
   2959         addAttendees(account, eventId, seed++);
   2960         addReminders(account, eventId, seed++);
   2961 
   2962         // Get some instances.
   2963         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
   2964         String testStart = "1999-01-01T00:00:00";
   2965         String testEnd = "1999-01-29T23:59:59";
   2966         String[] projection = { Instances.BEGIN, Instances.START_MINUTE };
   2967 
   2968         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
   2969                 new long[] { calendarId });
   2970         if (DEBUG_RECURRENCE) {
   2971             dumpInstances(instances, timeZone, "initial");
   2972         }
   2973 
   2974         assertEquals("initial recurrence instance count", 5, instances.getCount());
   2975 
   2976         // Modify starting from 3rd instance.
   2977         instances.moveToPosition(2);
   2978 
   2979         long startMillis;
   2980         ContentValues excepValues;
   2981 
   2982         // Replace with a new recurrence rule.  We move the start time an hour later, and cap
   2983         // it at two instances.
   2984         startMillis = instances.getLong(0);
   2985         excepValues = EventHelper.getNewExceptionValues(startMillis);
   2986         excepValues.put(Events.DTSTART, startMillis + 3600*1000);
   2987         excepValues.put(Events.RRULE, "FREQ=WEEKLY;COUNT=2;WKST=SU");
   2988         long excepEventId = createAndVerifyException(account, eventId, excepValues, true);
   2989         instances.close();
   2990 
   2991 
   2992         // Check to see if it took.
   2993         instances = getInstances(timeZone, testStart, testEnd, projection,
   2994                 new long[] { calendarId });
   2995         if (DEBUG_RECURRENCE) {
   2996             dumpInstances(instances, timeZone, "with new rule");
   2997         }
   2998 
   2999         assertEquals("count with exception", 4, instances.getCount());
   3000 
   3001         long prevMinute = -1;
   3002         for (int i = 0; i < 4; i++) {
   3003             long startMinute;
   3004             instances.moveToNext();
   3005             switch (i) {
   3006                 case 0:
   3007                     startMinute = instances.getLong(1);
   3008                     break;
   3009                 case 1:
   3010                 case 3:
   3011                     startMinute = instances.getLong(1);
   3012                     assertEquals("first/last pairs match", prevMinute, startMinute);
   3013                     break;
   3014                 case 2:
   3015                     startMinute = instances.getLong(1);
   3016                     assertFalse("first two != last two", prevMinute == startMinute);
   3017                     break;
   3018                 default:
   3019                     fail();
   3020                     startMinute = -1;   // make compiler happy
   3021                     break;
   3022             }
   3023 
   3024             prevMinute = startMinute;
   3025         }
   3026         instances.close();
   3027 
   3028         // delete the calendar
   3029         removeAndVerifyCalendar(account, calendarId);
   3030     }
   3031 
   3032     /**
   3033      * Tests exceptions that modify all instances of a recurring event.  This is not really an
   3034      * exception, since it won't create a new event, but supporting it allows us to use the
   3035      * exception URI without having to determine whether the "start from here" instance is the
   3036      * very first instance.
   3037      */
   3038     @MediumTest
   3039     public void testFullRecurrenceUpdate() {
   3040         String account = "ref_account";
   3041         int seed = 0;
   3042 
   3043         // Clean up just in case
   3044         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3045 
   3046         // Create calendar
   3047         long calendarId = createAndVerifyCalendar(account, seed++, null);
   3048 
   3049         // Create recurring event
   3050         String rrule = "FREQ=DAILY;WKST=MO;COUNT=100";
   3051         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
   3052                 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule);
   3053         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
   3054         //Log.i(TAG, "+++ eventId is " + eventId);
   3055 
   3056         // Get some instances.
   3057         String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
   3058         String testStart = "1997-08-01T00:00:00";
   3059         String testEnd = "1997-08-31T23:59:59";
   3060         String[] projection = { Instances.BEGIN, Instances.EVENT_LOCATION };
   3061         String newLocation = "NEW!";
   3062 
   3063         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
   3064                 new long[] { calendarId });
   3065         if (DEBUG_RECURRENCE) {
   3066             dumpInstances(instances, timeZone, "initial");
   3067         }
   3068 
   3069         assertEquals("initial recurrence instance count", 3, instances.getCount());
   3070 
   3071         instances.moveToFirst();
   3072         long startMillis = instances.getLong(0);
   3073         ContentValues excepValues = EventHelper.getNewExceptionValues(startMillis);
   3074         excepValues.put(Events.RRULE, rrule);   // identifies this as an "all future events" excep
   3075         excepValues.put(Events.EVENT_LOCATION, newLocation);
   3076         long excepEventId = createAndVerifyException(account, eventId, excepValues, true);
   3077         instances.close();
   3078 
   3079         // Check results.
   3080         assertEquals("full update does not create new ID", eventId, excepEventId);
   3081 
   3082         instances = getInstances(timeZone, testStart, testEnd, projection,
   3083                 new long[] { calendarId });
   3084         assertEquals("post-update instance count", 3, instances.getCount());
   3085         while (instances.moveToNext()) {
   3086             assertEquals("new location", newLocation, instances.getString(1));
   3087         }
   3088         instances.close();
   3089 
   3090         // delete the calendar
   3091         removeAndVerifyCalendar(account, calendarId);
   3092     }
   3093 
   3094     @MediumTest
   3095     public void testMultiRuleRecurrence() {
   3096         String account = "multirule_account";
   3097         int seed = 0;
   3098 
   3099         // Clean up just in case
   3100         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3101 
   3102         // Create calendar
   3103         long calendarId = createAndVerifyCalendar(account, seed++, null);
   3104 
   3105         // Create recurring event
   3106         String rrule = "FREQ=DAILY;WKST=MO;COUNT=5\nFREQ=WEEKLY;WKST=SU;COUNT=5";
   3107         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
   3108                 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule);
   3109         long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
   3110 
   3111         // TODO: once multi-rule RRULEs are fully supported, verify that they work
   3112 
   3113         // delete the calendar
   3114         removeAndVerifyCalendar(account, calendarId);
   3115     }
   3116 
   3117     /**
   3118      * Issue bad requests and expect them to get rejected.
   3119      */
   3120     @MediumTest
   3121     public void testBadRequests() {
   3122         String account = "neg_account";
   3123         int seed = 0;
   3124 
   3125         // Clean up just in case
   3126         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3127 
   3128         // Create calendar
   3129         long calendarId = createAndVerifyCalendar(account, seed++, null);
   3130 
   3131         // Create recurring event
   3132         String rrule = "FREQ=OFTEN;WKST=MO";
   3133         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++,
   3134                 calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule);
   3135         try {
   3136             createAndVerifyEvent(account, seed++, calendarId, true, eventValues);
   3137             fail("Bad recurrence rule should have been rejected");
   3138         } catch (IllegalArgumentException iae) {
   3139             // good
   3140         }
   3141 
   3142         // delete the calendar
   3143         removeAndVerifyCalendar(account, calendarId);
   3144     }
   3145 
   3146     /**
   3147      * Tests correct behavior of Calendars.isPrimary column
   3148      */
   3149     @MediumTest
   3150     public void testCalendarIsPrimary() {
   3151         String account = "ec_account";
   3152         int seed = 0;
   3153 
   3154         // Clean up just in case
   3155         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3156 
   3157         int isPrimary;
   3158         Cursor cursor;
   3159         ContentValues values = new ContentValues();
   3160 
   3161         final long calendarId = createAndVerifyCalendar(account, seed++, null);
   3162         final Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId);
   3163 
   3164         // verify when ownerAccount != account_name && isPrimary IS NULL
   3165         cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null);
   3166         cursor.moveToFirst();
   3167         isPrimary = cursor.getInt(0);
   3168         cursor.close();
   3169         assertEquals("isPrimary should be 0 if ownerAccount != account_name", 0, isPrimary);
   3170 
   3171         // verify when ownerAccount == account_name && isPrimary IS NULL
   3172         values.clear();
   3173         values.put(Calendars.OWNER_ACCOUNT, account);
   3174         mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null);
   3175         cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null);
   3176         cursor.moveToFirst();
   3177         isPrimary = cursor.getInt(0);
   3178         cursor.close();
   3179         assertEquals("isPrimary should be 1 if ownerAccount == account_name", 1, isPrimary);
   3180 
   3181         // verify isPrimary IS NOT NULL
   3182         values.clear();
   3183         values.put(Calendars.IS_PRIMARY, SOME_ARBITRARY_INT);
   3184         mContentResolver.update(uri, values, null, null);
   3185         cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null);
   3186         cursor.moveToFirst();
   3187         isPrimary = cursor.getInt(0);
   3188         cursor.close();
   3189         assertEquals("isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isPrimary);
   3190 
   3191         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3192     }
   3193 
   3194     /**
   3195      * Tests correct behavior of Events.isOrganizer column
   3196      */
   3197     @MediumTest
   3198     public void testEventsIsOrganizer() {
   3199         String account = "ec_account";
   3200         int seed = 0;
   3201 
   3202         // Clean up just in case
   3203         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3204 
   3205         int isOrganizer;
   3206         Cursor cursor;
   3207         ContentValues values = new ContentValues();
   3208 
   3209         final long calendarId = createAndVerifyCalendar(account, seed++, null);
   3210         final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null);
   3211         final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
   3212 
   3213         // verify when ownerAccount != organizer && isOrganizer IS NULL
   3214         cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null);
   3215         cursor.moveToFirst();
   3216         isOrganizer = cursor.getInt(0);
   3217         cursor.close();
   3218         assertEquals("isOrganizer should be 0 if ownerAccount != organizer", 0, isOrganizer);
   3219 
   3220         // verify when ownerAccount == account_name && isOrganizer IS NULL
   3221         values.clear();
   3222         values.put(Events.ORGANIZER, CalendarHelper.generateCalendarOwnerEmail(account));
   3223         mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null);
   3224         cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null);
   3225         cursor.moveToFirst();
   3226         isOrganizer = cursor.getInt(0);
   3227         cursor.close();
   3228         assertEquals("isOrganizer should be 1 if ownerAccount == organizer", 1, isOrganizer);
   3229 
   3230         // verify isOrganizer IS NOT NULL
   3231         values.clear();
   3232         values.put(Events.IS_ORGANIZER, SOME_ARBITRARY_INT);
   3233         mContentResolver.update(uri, values, null, null);
   3234         cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null);
   3235         cursor.moveToFirst();
   3236         isOrganizer = cursor.getInt(0);
   3237         cursor.close();
   3238         assertEquals(
   3239                 "isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isOrganizer);
   3240         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3241     }
   3242 
   3243     /**
   3244      * Tests correct behavior of Events.uid2445 column
   3245      */
   3246     @MediumTest
   3247     public void testEventsUid2445() {
   3248         String account = "ec_account";
   3249         int seed = 0;
   3250 
   3251         // Clean up just in case
   3252         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3253 
   3254         final String uid = "uid_123";
   3255         Cursor cursor;
   3256         ContentValues values = new ContentValues();
   3257         final long calendarId = createAndVerifyCalendar(account, seed++, null);
   3258         final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null);
   3259         final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
   3260 
   3261         // Verify default is null
   3262         cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null);
   3263         cursor.moveToFirst();
   3264         assertTrue(cursor.isNull(0));
   3265         cursor.close();
   3266 
   3267         // Write column value and read back
   3268         values.clear();
   3269         values.put(Events.UID_2445, uid);
   3270         mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null);
   3271         cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null);
   3272         cursor.moveToFirst();
   3273         assertFalse(cursor.isNull(0));
   3274         assertEquals("Column uid_2445 has unexpected value.", uid, cursor.getString(0));
   3275 
   3276         CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
   3277     }
   3278 
   3279     /**
   3280      * Acquires the set of instances that appear between the specified start and end points.
   3281      *
   3282      * @param timeZone Time zone to use when parsing startWhen and endWhen
   3283      * @param startWhen Start date/time, in RFC 3339 format
   3284      * @param endWhen End date/time, in RFC 3339 format
   3285      * @param projection Array of desired column names
   3286      * @return Cursor with instances (caller should close when done)
   3287      */
   3288     private Cursor getInstances(String timeZone, String startWhen, String endWhen,
   3289             String[] projection, long[] calendarIds) {
   3290         Time startTime = new Time(timeZone);
   3291         startTime.parse3339(startWhen);
   3292         long startMillis = startTime.toMillis(false);
   3293 
   3294         Time endTime = new Time(timeZone);
   3295         endTime.parse3339(endWhen);
   3296         long endMillis = endTime.toMillis(false);
   3297 
   3298         // We want a list of instances that occur between the specified dates.  Use the
   3299         // "instances/when" URI.
   3300         Uri uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI,
   3301                 startMillis + "/" + endMillis);
   3302 
   3303         String where = null;
   3304         for (int i = 0; i < calendarIds.length; i++) {
   3305             if (i > 0) {
   3306                 where += " OR ";
   3307             } else {
   3308                 where = "";
   3309             }
   3310             where += (Instances.CALENDAR_ID + "=" + calendarIds[i]);
   3311         }
   3312         Cursor instances = mContentResolver.query(uri, projection, where, null,
   3313                 projection[0] + " ASC");
   3314 
   3315         return instances;
   3316     }
   3317 
   3318     /**
   3319      * Acquires the set of instances that appear between the specified start and end points
   3320      * that match the search terms.
   3321      *
   3322      * @param timeZone Time zone to use when parsing startWhen and endWhen
   3323      * @param startWhen Start date/time, in RFC 3339 format
   3324      * @param endWhen End date/time, in RFC 3339 format
   3325      * @param search A collection of tokens to search for.  The columns searched are
   3326      *   hard-coded in the provider (currently title, description, location, attendee
   3327      *   name, attendee email).
   3328      * @param searchByDay If set, adjust start/end to calendar day boundaries.
   3329      * @param projection Array of desired column names
   3330      * @return Cursor with instances (caller should close when done)
   3331      */
   3332     private Cursor getInstancesSearch(String timeZone, String startWhen, String endWhen,
   3333             String search, boolean searchByDay, String[] projection, long[] calendarIds) {
   3334         Time startTime = new Time(timeZone);
   3335         startTime.parse3339(startWhen);
   3336         long startMillis = startTime.toMillis(false);
   3337 
   3338         Time endTime = new Time(timeZone);
   3339         endTime.parse3339(endWhen);
   3340         long endMillis = endTime.toMillis(false);
   3341 
   3342         Uri uri;
   3343         if (searchByDay) {
   3344             // start/end are Julian day numbers rather than time in milliseconds
   3345             int julianStart = Time.getJulianDay(startMillis, startTime.gmtoff);
   3346             int julianEnd = Time.getJulianDay(endMillis, endTime.gmtoff);
   3347             uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_BY_DAY_URI,
   3348                     julianStart + "/" + julianEnd + "/" + search);
   3349         } else {
   3350             uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_URI,
   3351                     startMillis + "/" + endMillis + "/" + search);
   3352         }
   3353 
   3354         String where = null;
   3355         for (int i = 0; i < calendarIds.length; i++) {
   3356             if (i > 0) {
   3357                 where += " OR ";
   3358             } else {
   3359                 where = "";
   3360             }
   3361             where += (Instances.CALENDAR_ID + "=" + calendarIds[i]);
   3362         }
   3363         // We want a list of instances that occur between the specified dates and that match
   3364         // the search terms.
   3365 
   3366         Cursor instances = mContentResolver.query(uri, projection, where, null,
   3367                 projection[0] + " ASC");
   3368 
   3369         return instances;
   3370     }
   3371 
   3372     /** debug -- dump instances cursor */
   3373     private static void dumpInstances(Cursor instances, String timeZone, String msg) {
   3374         Log.d(TAG, "Instances (" + msg + ")");
   3375 
   3376         int posn = instances.getPosition();
   3377         instances.moveToPosition(-1);
   3378 
   3379         //Log.d(TAG, "+++ instances has " + instances.getCount() + " rows, " +
   3380         //        instances.getColumnCount() + " columns");
   3381         while (instances.moveToNext()) {
   3382             long beginMil = instances.getLong(0);
   3383             Time beginT = new Time(timeZone);
   3384             beginT.set(beginMil);
   3385             String logMsg = "--> begin=" + beginT.format3339(false) + " (" + beginMil + ")";
   3386             for (int i = 2; i < instances.getColumnCount(); i++) {
   3387                 logMsg += " [" + instances.getString(i) + "]";
   3388             }
   3389             Log.d(TAG, logMsg);
   3390         }
   3391         instances.moveToPosition(posn);
   3392     }
   3393 
   3394 
   3395     /**
   3396      * Counts the number of instances that appear between the specified start and end times.
   3397      */
   3398     private int getInstanceCount(String timeZone, String startWhen, String endWhen,
   3399                 long[] calendarIds) {
   3400         Cursor instances = getInstances(timeZone, startWhen, endWhen,
   3401                 new String[] { Instances._ID }, calendarIds);
   3402         int count = instances.getCount();
   3403         instances.close();
   3404         return count;
   3405     }
   3406 
   3407     /**
   3408      * Deletes an event as app and sync adapter which removes it from the db and
   3409      * verifies after each.
   3410      *
   3411      * @param eventUri The uri for the event to delete
   3412      * @param accountName TODO
   3413      */
   3414     private void removeAndVerifyEvent(Uri eventUri, ContentValues eventValues, String accountName) {
   3415         // Delete event
   3416         EventHelper.deleteEvent(mContentResolver, eventUri, eventValues);
   3417         // Verify
   3418         verifyEvent(eventValues, ContentUris.parseId(eventUri));
   3419         // Delete as sync adapter
   3420         assertEquals(1,
   3421                 EventHelper.deleteEventAsSyncAdapter(mContentResolver, eventUri, accountName));
   3422         // Verify
   3423         Cursor c = EventHelper.getEventByUri(mContentResolver, eventUri);
   3424         assertEquals(0, c.getCount());
   3425         c.close();
   3426     }
   3427 
   3428     /**
   3429      * Creates an event on the given calendar and verifies it.
   3430      *
   3431      * @param account
   3432      * @param seed
   3433      * @param calendarId
   3434      * @param asSyncAdapter
   3435      * @param values optional pre created set of values; will have several new entries added
   3436      * @return the _id for the new event
   3437      */
   3438     private long createAndVerifyEvent(String account, int seed, long calendarId,
   3439             boolean asSyncAdapter, ContentValues values) {
   3440         // Create an event
   3441         if (values == null) {
   3442             values = EventHelper.getNewEventValues(account, seed, calendarId, asSyncAdapter);
   3443         }
   3444         Uri insertUri = Events.CONTENT_URI;
   3445         if (asSyncAdapter) {
   3446             insertUri = asSyncAdapter(insertUri, account, CTS_TEST_TYPE);
   3447         }
   3448         Uri uri = mContentResolver.insert(insertUri, values);
   3449         assertNotNull(uri);
   3450 
   3451         // Verify
   3452         EventHelper.addDefaultReadOnlyValues(values, account, asSyncAdapter);
   3453         long eventId = ContentUris.parseId(uri);
   3454         assertTrue(eventId >= 0);
   3455 
   3456         verifyEvent(values, eventId);
   3457         return eventId;
   3458     }
   3459 
   3460     /**
   3461      * Updates an event, and verifies that the updates took.
   3462      */
   3463     private void updateAndVerifyEvent(String account, long calendarId, long eventId,
   3464             boolean asSyncAdapter, ContentValues updateValues) {
   3465         Uri uri = Uri.withAppendedPath(Events.CONTENT_URI, String.valueOf(eventId));
   3466         if (asSyncAdapter) {
   3467             uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
   3468         }
   3469         int count = mContentResolver.update(uri, updateValues, null, null);
   3470 
   3471         // Verify
   3472         assertEquals(1, count);
   3473         verifyEvent(updateValues, eventId);
   3474     }
   3475 
   3476     /**
   3477      * Creates an exception to a recurring event, and verifies it.
   3478      * @param account The account to use.
   3479      * @param originalEventId The ID of the original event.
   3480      * @param values Values for the exception; must include originalInstanceTime.
   3481      * @return The _id for the new event.
   3482      */
   3483     private long createAndVerifyException(String account, long originalEventId,
   3484             ContentValues values, boolean asSyncAdapter) {
   3485         // Create the exception
   3486         Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
   3487                 String.valueOf(originalEventId));
   3488         if (asSyncAdapter) {
   3489             uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
   3490         }
   3491         Uri resultUri = mContentResolver.insert(uri, values);
   3492         assertNotNull(resultUri);
   3493         long eventId = ContentUris.parseId(resultUri);
   3494         assertTrue(eventId >= 0);
   3495         return eventId;
   3496     }
   3497 
   3498     /**
   3499      * Deletes an exception to a recurring event.
   3500      * @param account The account to use.
   3501      * @param eventId The ID of the original recurring event.
   3502      * @param excepId The ID of the exception event.
   3503      * @return The number of rows deleted.
   3504      */
   3505     private int deleteException(String account, long eventId, long excepId) {
   3506         Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI,
   3507                 eventId + "/" + excepId);
   3508         uri = asSyncAdapter(uri, account, CTS_TEST_TYPE);
   3509         return mContentResolver.delete(uri, null, null);
   3510     }
   3511 
   3512     /**
   3513      * Add some sample attendees to an event.
   3514      */
   3515     private void addAttendees(String account, long eventId, int seed) {
   3516         assertTrue(eventId >= 0);
   3517         AttendeeHelper.addAttendee(mContentResolver, eventId,
   3518                 "Attender" + seed,
   3519                 CalendarHelper.generateCalendarOwnerEmail(account),
   3520                 Attendees.ATTENDEE_STATUS_ACCEPTED,
   3521                 Attendees.RELATIONSHIP_ORGANIZER,
   3522                 Attendees.TYPE_NONE);
   3523         seed++;
   3524 
   3525         AttendeeHelper.addAttendee(mContentResolver, eventId,
   3526                 "Attender" + seed,
   3527                 "attender" + seed + "@example.com",
   3528                 Attendees.ATTENDEE_STATUS_TENTATIVE,
   3529                 Attendees.RELATIONSHIP_NONE,
   3530                 Attendees.TYPE_NONE);
   3531     }
   3532 
   3533     /**
   3534      * Add some sample reminders to an event.
   3535      */
   3536     private void addReminders(String account, long eventId, int seed) {
   3537         ReminderHelper.addReminder(mContentResolver, eventId, seed * 5, Reminders.METHOD_ALERT);
   3538     }
   3539 
   3540     /**
   3541      * Creates and removes an event that covers a specific range of dates.  Call this to
   3542      * cause the provider to expand the CalendarMetaData min/max values to include the range.
   3543      * Useful when you want to see the provider expand the instances as the events are added.
   3544      */
   3545     private void expandInstanceRange(String account, long calendarId, String testStart,
   3546             String testEnd, String timeZone) {
   3547         int seed = 0;
   3548 
   3549         // TODO: this should use an UNTIL rule based on testEnd, not a COUNT
   3550         ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed,
   3551                 calendarId, true, testStart, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=100");
   3552 
   3553         /*
   3554          * Some of the helper functions modify "eventValues", so we want to make sure we're
   3555          * passing a copy of anything we want to re-use.
   3556          */
   3557         long eventId = createAndVerifyEvent(account, seed, calendarId, true,
   3558                 new ContentValues(eventValues));
   3559         assertTrue(eventId >= 0);
   3560 
   3561         String[] projection = { Instances.BEGIN, Instances.START_MINUTE };
   3562         Cursor instances = getInstances(timeZone, testStart, testEnd, projection,
   3563                 new long[] { calendarId });
   3564         if (DEBUG_RECURRENCE) {
   3565             dumpInstances(instances, timeZone, "prep-create");
   3566         }
   3567         assertEquals("initial recurrence instance count", 3, instances.getCount());
   3568         instances.close();
   3569 
   3570         Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
   3571         removeAndVerifyEvent(eventUri, new ContentValues(eventValues), account);
   3572 
   3573         instances = getInstances(timeZone, testStart, testEnd, projection,
   3574                 new long[] { calendarId });
   3575         if (DEBUG_RECURRENCE) {
   3576             dumpInstances(instances, timeZone, "prep-clear");
   3577         }
   3578         assertEquals("initial recurrence instance count", 0, instances.getCount());
   3579         instances.close();
   3580 
   3581     }
   3582 
   3583     /**
   3584      * Inserts a new calendar with the given account and seed and verifies it.
   3585      *
   3586      * @param account The account to add the calendar to
   3587      * @param seed A number to use to generate the values
   3588      * @return the created calendar's id
   3589      */
   3590     private long createAndVerifyCalendar(String account, int seed, ContentValues values) {
   3591         // Create a calendar
   3592         if (values == null) {
   3593             values = CalendarHelper.getNewCalendarValues(account, seed);
   3594         }
   3595         Uri syncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE);
   3596         Uri uri = mContentResolver.insert(syncUri, values);
   3597         long calendarId = ContentUris.parseId(uri);
   3598         assertTrue(calendarId >= 0);
   3599 
   3600         verifyCalendar(account, values, calendarId, 1);
   3601         return calendarId;
   3602     }
   3603 
   3604     /**
   3605      * Deletes a given calendar and verifies no calendars remain on that
   3606      * account.
   3607      *
   3608      * @param account
   3609      * @param id
   3610      */
   3611     private void removeAndVerifyCalendar(String account, long id) {
   3612         // TODO Add code to delete as app and sync adapter and test both
   3613 
   3614         // Delete
   3615         assertEquals(1, CalendarHelper.deleteCalendarById(mContentResolver, id));
   3616 
   3617         // Verify
   3618         Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account);
   3619         assertEquals(0, c.getCount());
   3620         c.close();
   3621     }
   3622 
   3623     /**
   3624      * Check all the fields of a calendar contained in values + id.
   3625      *
   3626      * @param account the account of the calendar
   3627      * @param values the values to check against the db
   3628      * @param id the _id of the calendar
   3629      * @param expectedCount the number of calendars expected on this account
   3630      */
   3631     private void verifyCalendar(String account, ContentValues values, long id, int expectedCount) {
   3632         // Verify
   3633         Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account);
   3634         assertEquals(expectedCount, c.getCount());
   3635         assertTrue(c.moveToFirst());
   3636         while (c.getLong(0) != id) {
   3637             assertTrue(c.moveToNext());
   3638         }
   3639         for (String key : values.keySet()) {
   3640             int index = c.getColumnIndex(key);
   3641             assertTrue("Key " + key + " not in projection", index >= 0);
   3642             assertEquals(key, values.getAsString(key), c.getString(index));
   3643         }
   3644         c.close();
   3645     }
   3646 
   3647     /**
   3648      * Creates a new _sync_state entry and verifies the contents.
   3649      */
   3650     private long createAndVerifySyncState(String account, ContentValues values) {
   3651         assertNotNull(values);
   3652         Uri syncUri = asSyncAdapter(SyncState.CONTENT_URI, account, CTS_TEST_TYPE);
   3653         Uri uri = mContentResolver.insert(syncUri, values);
   3654         long syncStateId = ContentUris.parseId(uri);
   3655         assertTrue(syncStateId >= 0);
   3656 
   3657         verifySyncState(account, values, syncStateId);
   3658         return syncStateId;
   3659 
   3660     }
   3661 
   3662     /**
   3663      * Removes the _sync_state entry with the specified id, then verifies that it's gone.
   3664      */
   3665     private void removeAndVerifySyncState(String account) {
   3666         assertEquals(1, SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true));
   3667 
   3668         // Verify
   3669         Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account);
   3670         try {
   3671             assertEquals(0, c.getCount());
   3672         } finally {
   3673             if (c != null) {
   3674                 c.close();
   3675             }
   3676         }
   3677     }
   3678 
   3679     /**
   3680      * Check all the fields of a _sync_state entry contained in values + id. This assumes
   3681      * a single _sync_state has been created on the given account.
   3682      */
   3683     private void verifySyncState(String account, ContentValues values, long id) {
   3684         // Verify
   3685         Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account);
   3686         try {
   3687             assertEquals(1, c.getCount());
   3688             assertTrue(c.moveToFirst());
   3689             assertEquals(id, c.getLong(0));
   3690             for (String key : values.keySet()) {
   3691                 int index = c.getColumnIndex(key);
   3692                 if (key.equals(SyncState.DATA)) {
   3693                     // TODO: can't compare as string, so compare as byte[]
   3694                 } else {
   3695                     assertEquals(key, values.getAsString(key), c.getString(index));
   3696                 }
   3697             }
   3698         } finally {
   3699             if (c != null) {
   3700                 c.close();
   3701             }
   3702         }
   3703     }
   3704 
   3705 
   3706     /**
   3707      * Special version of the test runner that does some remote Emma coverage housekeeping.
   3708      */
   3709     public static class CalendarEmmaTestRunner extends InstrumentationCtsTestRunner {
   3710         private static final Uri EMMA_CONTENT_URI =
   3711             Uri.parse("content://" + CalendarContract.AUTHORITY + "/emma");
   3712         private ContentResolver mContentResolver;
   3713 
   3714         @Override
   3715         public void onStart() {
   3716             mContentResolver = getTargetContext().getContentResolver();
   3717 
   3718             ContentValues values = new ContentValues();
   3719             values.put("cmd", "start");
   3720             mContentResolver.insert(EMMA_CONTENT_URI, values);
   3721 
   3722             super.onStart();
   3723         }
   3724 
   3725         @Override
   3726         public void finish(int resultCode, Bundle results) {
   3727             ContentValues values = new ContentValues();
   3728             values.put("cmd", "stop");
   3729             values.put("outputFileName",
   3730                     Environment.getExternalStorageDirectory() + "/calendar-provider.ec");
   3731             mContentResolver.insert(EMMA_CONTENT_URI, values);
   3732             super.finish(resultCode, results);
   3733         }
   3734     }
   3735 }
   3736