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