| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.provider.cts; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Entity; |
| import android.content.EntityIterator; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.cts.util.PollingCheck; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.provider.CalendarContract; |
| import android.provider.CalendarContract.Attendees; |
| import android.provider.CalendarContract.CalendarEntity; |
| import android.provider.CalendarContract.Calendars; |
| import android.provider.CalendarContract.Colors; |
| import android.provider.CalendarContract.Events; |
| import android.provider.CalendarContract.EventsEntity; |
| import android.provider.CalendarContract.ExtendedProperties; |
| import android.provider.CalendarContract.Instances; |
| import android.provider.CalendarContract.Reminders; |
| import android.provider.CalendarContract.SyncState; |
| import android.test.InstrumentationTestCase; |
| import android.test.suitebuilder.annotation.MediumTest; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.text.format.Time; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| public class CalendarTest extends InstrumentationTestCase { |
| |
| private static final String TAG = "CalCTS"; |
| private static final String CTS_TEST_TYPE = "LOCAL"; |
| |
| // an arbitrary int used by some tests |
| private static final int SOME_ARBITRARY_INT = 143234; |
| |
| // 10 sec timeout for reminder broadcast (but shouldn't usually take this long). |
| private static final int POLLING_TIMEOUT = 10000; |
| |
| // @formatter:off |
| private static final String[] TIME_ZONES = new String[] { |
| "UTC", |
| "America/Los_Angeles", |
| "Asia/Beirut", |
| "Pacific/Auckland", }; |
| // @formatter:on |
| |
| private static final String SQL_WHERE_ID = Events._ID + "=?"; |
| private static final String SQL_WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?"; |
| |
| private ContentResolver mContentResolver; |
| |
| /** If set, log verbose instance info when running recurrence tests. */ |
| private static final boolean DEBUG_RECURRENCE = false; |
| |
| private static class CalendarHelper { |
| |
| // @formatter:off |
| public static final String[] CALENDARS_SYNC_PROJECTION = new String[] { |
| Calendars._ID, |
| Calendars.ACCOUNT_NAME, |
| Calendars.ACCOUNT_TYPE, |
| Calendars._SYNC_ID, |
| Calendars.CAL_SYNC7, |
| Calendars.CAL_SYNC8, |
| Calendars.DIRTY, |
| Calendars.NAME, |
| Calendars.CALENDAR_DISPLAY_NAME, |
| Calendars.CALENDAR_COLOR, |
| Calendars.CALENDAR_COLOR_KEY, |
| Calendars.CALENDAR_ACCESS_LEVEL, |
| Calendars.VISIBLE, |
| Calendars.SYNC_EVENTS, |
| Calendars.CALENDAR_LOCATION, |
| Calendars.CALENDAR_TIME_ZONE, |
| Calendars.OWNER_ACCOUNT, |
| Calendars.CAN_ORGANIZER_RESPOND, |
| Calendars.CAN_MODIFY_TIME_ZONE, |
| Calendars.MAX_REMINDERS, |
| Calendars.ALLOWED_REMINDERS, |
| Calendars.ALLOWED_AVAILABILITY, |
| Calendars.ALLOWED_ATTENDEE_TYPES, |
| Calendars.DELETED, |
| Calendars.CAL_SYNC1, |
| Calendars.CAL_SYNC2, |
| Calendars.CAL_SYNC3, |
| Calendars.CAL_SYNC4, |
| Calendars.CAL_SYNC5, |
| Calendars.CAL_SYNC6, |
| }; |
| // @formatter:on |
| |
| private CalendarHelper() {} // do not instantiate this class |
| |
| /** |
| * Generates the e-mail address for the Calendar owner. Use this for |
| * Calendars.OWNER_ACCOUNT, Events.OWNER_ACCOUNT, and for Attendees.ATTENDEE_EMAIL |
| * when you want a "self" attendee entry. |
| */ |
| static String generateCalendarOwnerEmail(String account) { |
| return "OWNER_" + account + "@example.com"; |
| } |
| |
| /** |
| * Creates a new set of values for creating a single calendar with every |
| * field. |
| * |
| * @param account The account name to create this calendar with |
| * @param seed A number used to generate the values |
| * @return A complete set of values for the calendar |
| */ |
| public static ContentValues getNewCalendarValues( |
| String account, int seed) { |
| String seedString = Long.toString(seed); |
| ContentValues values = new ContentValues(); |
| values.put(Calendars.ACCOUNT_TYPE, CTS_TEST_TYPE); |
| |
| values.put(Calendars.ACCOUNT_NAME, account); |
| values.put(Calendars._SYNC_ID, "SYNC_ID:" + seedString); |
| values.put(Calendars.CAL_SYNC7, "SYNC_V:" + seedString); |
| values.put(Calendars.CAL_SYNC8, "SYNC_TIME:" + seedString); |
| values.put(Calendars.DIRTY, 0); |
| values.put(Calendars.OWNER_ACCOUNT, generateCalendarOwnerEmail(account)); |
| |
| values.put(Calendars.NAME, seedString); |
| values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString); |
| |
| values.put(Calendars.CALENDAR_ACCESS_LEVEL, (seed % 8) * 100); |
| |
| values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed); |
| values.put(Calendars.VISIBLE, seed % 2); |
| values.put(Calendars.SYNC_EVENTS, 1); // must be 1 for recurrence expansion |
| values.put(Calendars.CALENDAR_LOCATION, "LOCATION:" + seedString); |
| values.put(Calendars.CALENDAR_TIME_ZONE, TIME_ZONES[seed % TIME_ZONES.length]); |
| values.put(Calendars.CAN_ORGANIZER_RESPOND, seed % 2); |
| values.put(Calendars.CAN_MODIFY_TIME_ZONE, seed % 2); |
| values.put(Calendars.MAX_REMINDERS, 3); |
| values.put(Calendars.ALLOWED_REMINDERS, "0,1,2"); // does not include SMS (3) |
| values.put(Calendars.ALLOWED_ATTENDEE_TYPES, "0,1,2,3"); |
| values.put(Calendars.ALLOWED_AVAILABILITY, "0,1,2,3"); |
| values.put(Calendars.CAL_SYNC1, "SYNC1:" + seedString); |
| values.put(Calendars.CAL_SYNC2, "SYNC2:" + seedString); |
| values.put(Calendars.CAL_SYNC3, "SYNC3:" + seedString); |
| values.put(Calendars.CAL_SYNC4, "SYNC4:" + seedString); |
| values.put(Calendars.CAL_SYNC5, "SYNC5:" + seedString); |
| values.put(Calendars.CAL_SYNC6, "SYNC6:" + seedString); |
| |
| return values; |
| } |
| |
| /** |
| * Creates a set of values with just the updates and modifies the |
| * original values to the expected values |
| */ |
| public static ContentValues getUpdateCalendarValuesWithOriginal( |
| ContentValues original, int seed) { |
| ContentValues values = new ContentValues(); |
| String seedString = Long.toString(seed); |
| |
| values.put(Calendars.CALENDAR_DISPLAY_NAME, "DISPLAY_" + seedString); |
| values.put(Calendars.CALENDAR_COLOR, 0xff000000 + seed); |
| values.put(Calendars.VISIBLE, seed % 2); |
| values.put(Calendars.SYNC_EVENTS, seed % 2); |
| |
| original.putAll(values); |
| original.put(Calendars.DIRTY, 1); |
| |
| return values; |
| } |
| |
| public static int deleteCalendarById(ContentResolver resolver, long id) { |
| return resolver.delete(Calendars.CONTENT_URI, Calendars._ID + "=?", |
| new String[] { Long.toString(id) }); |
| } |
| |
| public static int deleteCalendarByAccount(ContentResolver resolver, String account) { |
| return resolver.delete(Calendars.CONTENT_URI, Calendars.ACCOUNT_NAME + "=?", |
| new String[] { account }); |
| } |
| |
| public static Cursor getCalendarsByAccount(ContentResolver resolver, String account) { |
| String selection = Calendars.ACCOUNT_TYPE + "=?"; |
| String[] selectionArgs; |
| if (account != null) { |
| selection += " AND " + Calendars.ACCOUNT_NAME + "=?"; |
| selectionArgs = new String[2]; |
| selectionArgs[1] = account; |
| } else { |
| selectionArgs = new String[1]; |
| } |
| selectionArgs[0] = CTS_TEST_TYPE; |
| |
| return resolver.query(Calendars.CONTENT_URI, CALENDARS_SYNC_PROJECTION, selection, |
| selectionArgs, null); |
| } |
| } |
| |
| /** |
| * Helper class for manipulating entries in the _sync_state table. |
| */ |
| private static class SyncStateHelper { |
| public static final String[] SYNCSTATE_PROJECTION = new String[] { |
| SyncState._ID, |
| SyncState.ACCOUNT_NAME, |
| SyncState.ACCOUNT_TYPE, |
| SyncState.DATA |
| }; |
| |
| private static final byte[] SAMPLE_SYNC_DATA = { |
| (byte) 'H', (byte) 'e', (byte) 'l', (byte) 'l', (byte) 'o' |
| }; |
| |
| private SyncStateHelper() {} // do not instantiate |
| |
| /** |
| * Creates a new set of values for creating a new _sync_state entry. |
| */ |
| public static ContentValues getNewSyncStateValues(String account) { |
| ContentValues values = new ContentValues(); |
| values.put(SyncState.DATA, SAMPLE_SYNC_DATA); |
| values.put(SyncState.ACCOUNT_NAME, account); |
| values.put(SyncState.ACCOUNT_TYPE, CTS_TEST_TYPE); |
| return values; |
| } |
| |
| /** |
| * Retrieves the _sync_state entry with the specified ID. |
| */ |
| public static Cursor getSyncStateById(ContentResolver resolver, long id) { |
| Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id); |
| return resolver.query(uri, SYNCSTATE_PROJECTION, null, null, null); |
| } |
| |
| /** |
| * Retrieves the _sync_state entry for the specified account. |
| */ |
| public static Cursor getSyncStateByAccount(ContentResolver resolver, String account) { |
| assertNotNull(account); |
| String selection = SyncState.ACCOUNT_TYPE + "=? AND " + SyncState.ACCOUNT_NAME + "=?"; |
| String[] selectionArgs = new String[] { CTS_TEST_TYPE, account }; |
| |
| return resolver.query(SyncState.CONTENT_URI, SYNCSTATE_PROJECTION, selection, |
| selectionArgs, null); |
| } |
| |
| /** |
| * Deletes the _sync_state entry with the specified ID. Always done as app. |
| */ |
| public static int deleteSyncStateById(ContentResolver resolver, long id) { |
| Uri uri = ContentUris.withAppendedId(SyncState.CONTENT_URI, id); |
| return resolver.delete(uri, null, null); |
| } |
| |
| /** |
| * Deletes the _sync_state entry associated with the specified account. Can be done |
| * as app or sync adapter. |
| */ |
| public static int deleteSyncStateByAccount(ContentResolver resolver, String account, |
| boolean asSyncAdapter) { |
| Uri uri = SyncState.CONTENT_URI; |
| if (asSyncAdapter) { |
| uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); |
| } |
| return resolver.delete(uri, SyncState.ACCOUNT_NAME + "=?", |
| new String[] { account }); |
| } |
| } |
| |
| // @formatter:off |
| private static class EventHelper { |
| public static final String[] EVENTS_PROJECTION = new String[] { |
| Events._ID, |
| Events.ACCOUNT_NAME, |
| Events.ACCOUNT_TYPE, |
| Events.OWNER_ACCOUNT, |
| // Events.ORGANIZER_CAN_RESPOND, from Calendars |
| // Events.CAN_CHANGE_TZ, from Calendars |
| // Events.MAX_REMINDERS, from Calendars |
| Events.CALENDAR_ID, |
| // Events.CALENDAR_DISPLAY_NAME, from Calendars |
| // Events.CALENDAR_COLOR, from Calendars |
| // Events.CALENDAR_ACL, from Calendars |
| // Events.CALENDAR_VISIBLE, from Calendars |
| Events.SYNC_DATA3, |
| Events.SYNC_DATA6, |
| Events.TITLE, |
| Events.EVENT_LOCATION, |
| Events.DESCRIPTION, |
| Events.STATUS, |
| Events.SELF_ATTENDEE_STATUS, |
| Events.DTSTART, |
| Events.DTEND, |
| Events.EVENT_TIMEZONE, |
| Events.EVENT_END_TIMEZONE, |
| Events.EVENT_COLOR, |
| Events.EVENT_COLOR_KEY, |
| Events.DURATION, |
| Events.ALL_DAY, |
| Events.ACCESS_LEVEL, |
| Events.AVAILABILITY, |
| Events.HAS_ALARM, |
| Events.HAS_EXTENDED_PROPERTIES, |
| Events.RRULE, |
| Events.RDATE, |
| Events.EXRULE, |
| Events.EXDATE, |
| Events.ORIGINAL_ID, |
| Events.ORIGINAL_SYNC_ID, |
| Events.ORIGINAL_INSTANCE_TIME, |
| Events.ORIGINAL_ALL_DAY, |
| Events.LAST_DATE, |
| Events.HAS_ATTENDEE_DATA, |
| Events.GUESTS_CAN_MODIFY, |
| Events.GUESTS_CAN_INVITE_OTHERS, |
| Events.GUESTS_CAN_SEE_GUESTS, |
| Events.ORGANIZER, |
| Events.DELETED, |
| Events._SYNC_ID, |
| Events.SYNC_DATA4, |
| Events.SYNC_DATA5, |
| Events.DIRTY, |
| Events.SYNC_DATA8, |
| Events.SYNC_DATA2, |
| Events.SYNC_DATA1, |
| Events.SYNC_DATA2, |
| Events.SYNC_DATA3, |
| Events.SYNC_DATA4, |
| Events.MUTATORS, |
| }; |
| // @formatter:on |
| |
| private EventHelper() {} // do not instantiate this class |
| |
| /** |
| * Constructs a set of name/value pairs that can be used to create a Calendar event. |
| * Various fields are generated from the seed value. |
| */ |
| public static ContentValues getNewEventValues( |
| String account, int seed, long calendarId, boolean asSyncAdapter) { |
| String seedString = Long.toString(seed); |
| ContentValues values = new ContentValues(); |
| values.put(Events.ORGANIZER, "ORGANIZER:" + seedString); |
| |
| values.put(Events.TITLE, "TITLE:" + seedString); |
| values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString); |
| |
| values.put(Events.CALENDAR_ID, calendarId); |
| |
| values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString); |
| values.put(Events.STATUS, seed % 2); // avoid STATUS_CANCELED for general testing |
| |
| values.put(Events.DTSTART, seed); |
| values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS); |
| values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]); |
| values.put(Events.EVENT_COLOR, seed); |
| // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) % |
| // TIME_ZONES.length]); |
| if ((seed % 2) == 0) { |
| // Either set to zero, or leave unset to get default zero. |
| // Must be 0 or dtstart/dtend will get adjusted. |
| values.put(Events.ALL_DAY, 0); |
| } |
| values.put(Events.ACCESS_LEVEL, seed % 4); |
| values.put(Events.AVAILABILITY, seed % 2); |
| values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2); |
| values.put(Events.HAS_ATTENDEE_DATA, seed % 2); |
| values.put(Events.GUESTS_CAN_MODIFY, seed % 2); |
| values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2); |
| values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2); |
| |
| // Default is STATUS_TENTATIVE (0). We either set it to that explicitly, or leave |
| // it set to the default. |
| if (seed != Events.STATUS_TENTATIVE) { |
| values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE); |
| } |
| |
| if (asSyncAdapter) { |
| values.put(Events._SYNC_ID, "SYNC_ID:" + seedString); |
| values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString); |
| values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString); |
| values.put(Events.SYNC_DATA3, "HTML:" + seedString); |
| values.put(Events.SYNC_DATA6, "COMMENTS:" + seedString); |
| values.put(Events.DIRTY, 0); |
| values.put(Events.SYNC_DATA8, "0"); |
| } else { |
| // only the sync adapter can set the DIRTY flag |
| //values.put(Events.DIRTY, 1); |
| } |
| // values.put(Events.SYNC1, "SYNC1:" + seedString); |
| // values.put(Events.SYNC2, "SYNC2:" + seedString); |
| // values.put(Events.SYNC3, "SYNC3:" + seedString); |
| // values.put(Events.SYNC4, "SYNC4:" + seedString); |
| // values.put(Events.SYNC5, "SYNC5:" + seedString); |
| // Events.RRULE, |
| // Events.RDATE, |
| // Events.EXRULE, |
| // Events.EXDATE, |
| // // Events.ORIGINAL_ID |
| // Events.ORIGINAL_EVENT, // rename ORIGINAL_SYNC_ID |
| // Events.ORIGINAL_INSTANCE_TIME, |
| // Events.ORIGINAL_ALL_DAY, |
| |
| return values; |
| } |
| |
| /** |
| * Constructs a set of name/value pairs that can be used to create a recurring |
| * Calendar event. |
| * |
| * A duration of "P1D" is treated as an all-day event. |
| * |
| * @param startWhen Starting date/time in RFC 3339 format |
| * @param duration Event duration, in RFC 2445 duration format |
| * @param rrule Recurrence rule |
| * @return name/value pairs to use when creating event |
| */ |
| public static ContentValues getNewRecurringEventValues(String account, int seed, |
| long calendarId, boolean asSyncAdapter, String startWhen, String duration, |
| String rrule) { |
| |
| // Set up some general stuff. |
| ContentValues values = getNewEventValues(account, seed, calendarId, asSyncAdapter); |
| |
| // Replace the DTSTART field. |
| String timeZone = values.getAsString(Events.EVENT_TIMEZONE); |
| Time time = new Time(timeZone); |
| time.parse3339(startWhen); |
| values.put(Events.DTSTART, time.toMillis(false)); |
| |
| // Add in the recurrence-specific fields, and drop DTEND. |
| values.put(Events.RRULE, rrule); |
| values.put(Events.DURATION, duration); |
| values.remove(Events.DTEND); |
| |
| return values; |
| } |
| |
| /** |
| * Constructs the basic name/value pairs required for an exception to a recurring event. |
| * |
| * @param instanceStartMillis The start time of the instance |
| * @return name/value pairs to use when creating event |
| */ |
| public static ContentValues getNewExceptionValues(long instanceStartMillis) { |
| ContentValues values = new ContentValues(); |
| values.put(Events.ORIGINAL_INSTANCE_TIME, instanceStartMillis); |
| |
| return values; |
| } |
| |
| public static ContentValues getUpdateEventValuesWithOriginal(ContentValues original, |
| int seed, boolean asSyncAdapter) { |
| String seedString = Long.toString(seed); |
| ContentValues values = new ContentValues(); |
| |
| values.put(Events.TITLE, "TITLE:" + seedString); |
| values.put(Events.EVENT_LOCATION, "LOCATION_" + seedString); |
| values.put(Events.DESCRIPTION, "DESCRIPTION:" + seedString); |
| values.put(Events.STATUS, seed % 3); |
| |
| values.put(Events.DTSTART, seed); |
| values.put(Events.DTEND, seed + DateUtils.HOUR_IN_MILLIS); |
| values.put(Events.EVENT_TIMEZONE, TIME_ZONES[seed % TIME_ZONES.length]); |
| // values.put(Events.EVENT_TIMEZONE2, TIME_ZONES[(seed +1) % |
| // TIME_ZONES.length]); |
| values.put(Events.ACCESS_LEVEL, seed % 4); |
| values.put(Events.AVAILABILITY, seed % 2); |
| values.put(Events.HAS_EXTENDED_PROPERTIES, seed % 2); |
| values.put(Events.HAS_ATTENDEE_DATA, seed % 2); |
| values.put(Events.GUESTS_CAN_MODIFY, seed % 2); |
| values.put(Events.GUESTS_CAN_INVITE_OTHERS, seed % 2); |
| values.put(Events.GUESTS_CAN_SEE_GUESTS, seed % 2); |
| if (asSyncAdapter) { |
| values.put(Events._SYNC_ID, "SYNC_ID:" + seedString); |
| values.put(Events.SYNC_DATA4, "SYNC_V:" + seedString); |
| values.put(Events.SYNC_DATA5, "SYNC_TIME:" + seedString); |
| values.put(Events.DIRTY, 0); |
| } |
| original.putAll(values); |
| return values; |
| } |
| |
| public static void addDefaultReadOnlyValues(ContentValues values, String account, |
| boolean asSyncAdapter) { |
| values.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE); |
| values.put(Events.DELETED, 0); |
| values.put(Events.DIRTY, asSyncAdapter ? 0 : 1); |
| values.put(Events.OWNER_ACCOUNT, CalendarHelper.generateCalendarOwnerEmail(account)); |
| values.put(Events.ACCOUNT_TYPE, CTS_TEST_TYPE); |
| values.put(Events.ACCOUNT_NAME, account); |
| } |
| |
| /** |
| * Generates a RFC2445-format duration string. |
| */ |
| private static String generateDurationString(long durationMillis, boolean isAllDay) { |
| long durationSeconds = durationMillis / 1000; |
| |
| // The server may react differently to an all-day event specified as "P1D" than |
| // it will to "PT86400S"; see b/1594638. |
| if (isAllDay && (durationSeconds % 86400) == 0) { |
| return "P" + durationSeconds / 86400 + "D"; |
| } else { |
| return "PT" + durationSeconds + "S"; |
| } |
| } |
| |
| /** |
| * Deletes the event, and updates the values. |
| * @param resolver The resolver to issue the query against. |
| * @param uri The deletion URI. |
| * @param values Set of values to update (sets DELETED and DIRTY). |
| * @return The number of rows modified. |
| */ |
| public static int deleteEvent(ContentResolver resolver, Uri uri, ContentValues values) { |
| values.put(Events.DELETED, 1); |
| values.put(Events.DIRTY, 1); |
| return resolver.delete(uri, null, null); |
| } |
| |
| public static int deleteEventAsSyncAdapter(ContentResolver resolver, Uri uri, |
| String account) { |
| Uri syncUri = asSyncAdapter(uri, account, CTS_TEST_TYPE); |
| return resolver.delete(syncUri, null, null); |
| } |
| |
| public static Cursor getEventsByAccount(ContentResolver resolver, String account) { |
| String selection = Calendars.ACCOUNT_TYPE + "=?"; |
| String[] selectionArgs; |
| if (account != null) { |
| selection += " AND " + Calendars.ACCOUNT_NAME + "=?"; |
| selectionArgs = new String[2]; |
| selectionArgs[1] = account; |
| } else { |
| selectionArgs = new String[1]; |
| } |
| selectionArgs[0] = CTS_TEST_TYPE; |
| return resolver.query(Events.CONTENT_URI, EVENTS_PROJECTION, selection, selectionArgs, |
| null); |
| } |
| |
| public static Cursor getEventByUri(ContentResolver resolver, Uri uri) { |
| return resolver.query(uri, EVENTS_PROJECTION, null, null, null); |
| } |
| |
| /** |
| * Looks up the specified Event in the database and returns the "selfAttendeeStatus" |
| * value. |
| */ |
| public static int lookupSelfAttendeeStatus(ContentResolver resolver, long eventId) { |
| return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId, |
| Events.SELF_ATTENDEE_STATUS); |
| } |
| |
| /** |
| * Looks up the specified Event in the database and returns the "hasAlarm" |
| * value. |
| */ |
| public static int lookupHasAlarm(ContentResolver resolver, long eventId) { |
| return getIntFromDatabase(resolver, Events.CONTENT_URI, eventId, |
| Events.HAS_ALARM); |
| } |
| } |
| |
| /** |
| * Helper class for manipulating entries in the Attendees table. |
| */ |
| private static class AttendeeHelper { |
| public static final String[] ATTENDEES_PROJECTION = new String[] { |
| Attendees._ID, |
| Attendees.EVENT_ID, |
| Attendees.ATTENDEE_NAME, |
| Attendees.ATTENDEE_EMAIL, |
| Attendees.ATTENDEE_STATUS, |
| Attendees.ATTENDEE_RELATIONSHIP, |
| Attendees.ATTENDEE_TYPE |
| }; |
| // indexes into projection |
| public static final int ATTENDEES_ID_INDEX = 0; |
| public static final int ATTENDEES_EVENT_ID_INDEX = 1; |
| |
| // do not instantiate |
| private AttendeeHelper() {} |
| |
| /** |
| * Adds a new attendee to the specified event. |
| * |
| * @return the _id of the new attendee, or -1 on failure |
| */ |
| public static long addAttendee(ContentResolver resolver, long eventId, String name, |
| String email, int status, int relationship, int type) { |
| Uri uri = Attendees.CONTENT_URI; |
| |
| ContentValues attendee = new ContentValues(); |
| attendee.put(Attendees.EVENT_ID, eventId); |
| attendee.put(Attendees.ATTENDEE_NAME, name); |
| attendee.put(Attendees.ATTENDEE_EMAIL, email); |
| attendee.put(Attendees.ATTENDEE_STATUS, status); |
| attendee.put(Attendees.ATTENDEE_RELATIONSHIP, relationship); |
| attendee.put(Attendees.ATTENDEE_TYPE, type); |
| Uri result = resolver.insert(uri, attendee); |
| return ContentUris.parseId(result); |
| } |
| |
| /** |
| * Finds all Attendees rows for the specified event and email address. The returned |
| * cursor will use {@link AttendeeHelper#ATTENDEES_PROJECTION}. |
| */ |
| public static Cursor findAttendeesByEmail(ContentResolver resolver, long eventId, |
| String email) { |
| return resolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION, |
| Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_EMAIL + "=?", |
| new String[] { String.valueOf(eventId), email }, null); |
| } |
| } |
| |
| /** |
| * Helper class for manipulating entries in the Colors table. |
| */ |
| private static class ColorHelper { |
| public static final String WHERE_COLOR_ACCOUNT = Colors.ACCOUNT_NAME + "=? AND " |
| + Colors.ACCOUNT_TYPE + "=?"; |
| public static final String WHERE_COLOR_ACCOUNT_AND_INDEX = WHERE_COLOR_ACCOUNT + " AND " |
| + Colors.COLOR_KEY + "=?"; |
| |
| public static final String[] COLORS_PROJECTION = new String[] { |
| Colors._ID, // 0 |
| Colors.ACCOUNT_NAME, // 1 |
| Colors.ACCOUNT_TYPE, // 2 |
| Colors.DATA, // 3 |
| Colors.COLOR_TYPE, // 4 |
| Colors.COLOR_KEY, // 5 |
| Colors.COLOR, // 6 |
| }; |
| // indexes into projection |
| public static final int COLORS_ID_INDEX = 0; |
| public static final int COLORS_INDEX_INDEX = 5; |
| public static final int COLORS_COLOR_INDEX = 6; |
| |
| public static final int[] DEFAULT_TYPES = new int[] { |
| Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR, Colors.TYPE_CALENDAR, |
| Colors.TYPE_CALENDAR, Colors.TYPE_EVENT, Colors.TYPE_EVENT, Colors.TYPE_EVENT, |
| Colors.TYPE_EVENT, |
| }; |
| public static final int[] DEFAULT_COLORS = new int[] { |
| 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFAA00AA, 0xFF00AAAA, 0xFF333333, 0xFFAAAA00, |
| 0xFFAAAAAA, |
| }; |
| public static final String[] DEFAULT_INDICES = new String[] { |
| "000", "001", "010", "011", "100", "101", "110", "111", |
| }; |
| |
| public static final int C_COLOR_0 = 0; |
| public static final int C_COLOR_1 = 1; |
| public static final int C_COLOR_2 = 2; |
| public static final int C_COLOR_3 = 3; |
| public static final int E_COLOR_0 = 4; |
| public static final int E_COLOR_1 = 5; |
| public static final int E_COLOR_2 = 6; |
| public static final int E_COLOR_3 = 7; |
| |
| // do not instantiate |
| private ColorHelper() { |
| } |
| |
| /** |
| * Adds a new color to the colors table. |
| * |
| * @return the _id of the new color, or -1 on failure |
| */ |
| public static long addColor(ContentResolver resolver, String accountName, |
| String accountType, String data, String index, int type, int color) { |
| Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType); |
| |
| ContentValues colorValues = new ContentValues(); |
| colorValues.put(Colors.DATA, data); |
| colorValues.put(Colors.COLOR_KEY, index); |
| colorValues.put(Colors.COLOR_TYPE, type); |
| colorValues.put(Colors.COLOR, color); |
| Uri result = resolver.insert(uri, colorValues); |
| return ContentUris.parseId(result); |
| } |
| |
| /** |
| * Finds the color specified by an account name/type and a color index. |
| * The returned cursor will use {@link ColorHelper#COLORS_PROJECTION}. |
| */ |
| public static Cursor findColorByIndex(ContentResolver resolver, String accountName, |
| String accountType, String index) { |
| return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION, |
| WHERE_COLOR_ACCOUNT_AND_INDEX, |
| new String[] {accountName, accountType, index}, null); |
| } |
| |
| public static Cursor findColorsByAccount(ContentResolver resolver, String accountName, |
| String accountType) { |
| return resolver.query(Colors.CONTENT_URI, COLORS_PROJECTION, WHERE_COLOR_ACCOUNT, |
| new String[] { accountName, accountType }, null); |
| } |
| |
| /** |
| * Adds a default set of test colors to the Colors table under the given |
| * account. |
| * |
| * @return true if the default colors were added successfully |
| */ |
| public static boolean addDefaultColorsToAccount(ContentResolver resolver, |
| String accountName, String accountType) { |
| for (int i = 0; i < DEFAULT_INDICES.length; i++) { |
| long id = addColor(resolver, accountName, accountType, null, DEFAULT_INDICES[i], |
| DEFAULT_TYPES[i], DEFAULT_COLORS[i]); |
| if (id == -1) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public static void deleteColorsByAccount(ContentResolver resolver, String accountName, |
| String accountType) { |
| Uri uri = asSyncAdapter(Colors.CONTENT_URI, accountName, accountType); |
| resolver.delete(uri, WHERE_COLOR_ACCOUNT, new String[] { accountName, accountType }); |
| } |
| } |
| |
| |
| /** |
| * Helper class for manipulating entries in the Reminders table. |
| */ |
| private static class ReminderHelper { |
| public static final String[] REMINDERS_PROJECTION = new String[] { |
| Reminders._ID, |
| Reminders.EVENT_ID, |
| Reminders.MINUTES, |
| Reminders.METHOD |
| }; |
| // indexes into projection |
| public static final int REMINDERS_ID_INDEX = 0; |
| public static final int REMINDERS_EVENT_ID_INDEX = 1; |
| public static final int REMINDERS_MINUTES_INDEX = 2; |
| public static final int REMINDERS_METHOD_INDEX = 3; |
| |
| // do not instantiate |
| private ReminderHelper() {} |
| |
| /** |
| * Adds a new reminder to the specified event. |
| * |
| * @return the _id of the new reminder, or -1 on failure |
| */ |
| public static long addReminder(ContentResolver resolver, long eventId, int minutes, |
| int method) { |
| Uri uri = Reminders.CONTENT_URI; |
| |
| ContentValues reminder = new ContentValues(); |
| reminder.put(Reminders.EVENT_ID, eventId); |
| reminder.put(Reminders.MINUTES, minutes); |
| reminder.put(Reminders.METHOD, method); |
| Uri result = resolver.insert(uri, reminder); |
| return ContentUris.parseId(result); |
| } |
| |
| /** |
| * Finds all Reminders rows for the specified event. The returned cursor will use |
| * {@link ReminderHelper#REMINDERS_PROJECTION}. |
| */ |
| public static Cursor findRemindersByEventId(ContentResolver resolver, long eventId) { |
| return resolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, |
| Reminders.EVENT_ID + "=?", new String[] { String.valueOf(eventId) }, null); |
| } |
| |
| /** |
| * Looks up the specified Reminders row and returns the "method" value. |
| */ |
| public static int lookupMethod(ContentResolver resolver, long remId) { |
| return getIntFromDatabase(resolver, Reminders.CONTENT_URI, remId, |
| Reminders.METHOD); |
| } |
| } |
| |
| /** |
| * Helper class for manipulating entries in the ExtendedProperties table. |
| */ |
| private static class ExtendedPropertiesHelper { |
| public static final String[] EXTENDED_PROPERTIES_PROJECTION = new String[] { |
| ExtendedProperties._ID, |
| ExtendedProperties.EVENT_ID, |
| ExtendedProperties.NAME, |
| ExtendedProperties.VALUE |
| }; |
| // indexes into projection |
| public static final int EXTENDED_PROPERTIES_ID_INDEX = 0; |
| public static final int EXTENDED_PROPERTIES_EVENT_ID_INDEX = 1; |
| public static final int EXTENDED_PROPERTIES_NAME_INDEX = 2; |
| public static final int EXTENDED_PROPERTIES_VALUE_INDEX = 3; |
| |
| // do not instantiate |
| private ExtendedPropertiesHelper() {} |
| |
| /** |
| * Adds a new ExtendedProperty for the specified event. Runs as sync adapter. |
| * |
| * @return the _id of the new ExtendedProperty, or -1 on failure |
| */ |
| public static long addExtendedProperty(ContentResolver resolver, String account, |
| long eventId, String name, String value) { |
| Uri uri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE); |
| |
| ContentValues ep = new ContentValues(); |
| ep.put(ExtendedProperties.EVENT_ID, eventId); |
| ep.put(ExtendedProperties.NAME, name); |
| ep.put(ExtendedProperties.VALUE, value); |
| Uri result = resolver.insert(uri, ep); |
| return ContentUris.parseId(result); |
| } |
| |
| /** |
| * Finds all ExtendedProperties rows for the specified event. The returned cursor will |
| * use {@link ExtendedPropertiesHelper#EXTENDED_PROPERTIES_PROJECTION}. |
| */ |
| public static Cursor findExtendedPropertiesByEventId(ContentResolver resolver, |
| long eventId) { |
| return resolver.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROPERTIES_PROJECTION, |
| ExtendedProperties.EVENT_ID + "=?", |
| new String[] { String.valueOf(eventId) }, null); |
| } |
| |
| /** |
| * Finds an ExtendedProperties entry with a matching name for the specified event, and |
| * returns the value. Throws an exception if we don't find exactly one row. |
| */ |
| public static String lookupValueByName(ContentResolver resolver, long eventId, |
| String name) { |
| Cursor cursor = resolver.query(ExtendedProperties.CONTENT_URI, |
| EXTENDED_PROPERTIES_PROJECTION, |
| ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?", |
| new String[] { String.valueOf(eventId), name }, null); |
| |
| try { |
| if (cursor.getCount() != 1) { |
| throw new RuntimeException("Got " + cursor.getCount() + " results, expected 1"); |
| } |
| |
| cursor.moveToFirst(); |
| return cursor.getString(EXTENDED_PROPERTIES_VALUE_INDEX); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates an updated URI that includes query parameters that identify the source as a |
| * sync adapter. |
| */ |
| static Uri asSyncAdapter(Uri uri, String account, String accountType) { |
| return uri.buildUpon() |
| .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, |
| "true") |
| .appendQueryParameter(Calendars.ACCOUNT_NAME, account) |
| .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); |
| } |
| |
| /** |
| * Returns the value of the specified row and column in the Events table, as an integer. |
| * Throws an exception if the specified row or column doesn't exist or doesn't contain |
| * an integer (e.g. null entry). |
| */ |
| private static int getIntFromDatabase(ContentResolver resolver, Uri uri, long rowId, |
| String columnName) { |
| String[] projection = { columnName }; |
| String selection = SQL_WHERE_ID; |
| String[] selectionArgs = { String.valueOf(rowId) }; |
| |
| Cursor c = resolver.query(uri, projection, selection, selectionArgs, null); |
| try { |
| assertEquals(1, c.getCount()); |
| c.moveToFirst(); |
| return c.getInt(0); |
| } finally { |
| c.close(); |
| } |
| } |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| mContentResolver = getInstrumentation().getTargetContext().getContentResolver(); |
| } |
| |
| @MediumTest |
| public void testCalendarCreationAndDeletion() { |
| String account = "cc1_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| long id = createAndVerifyCalendar(account, seed++, null); |
| |
| removeAndVerifyCalendar(account, id); |
| } |
| |
| /** |
| * Tests whether the default projections work. We don't need to have any data in |
| * the calendar, since it's testing the database schema. |
| */ |
| @MediumTest |
| public void testDefaultProjections() { |
| String account = "dproj_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| long id = createAndVerifyCalendar(account, seed++, null); |
| |
| Cursor c; |
| Uri uri; |
| // Calendars |
| c = mContentResolver.query(Calendars.CONTENT_URI, null, null, null, null); |
| c.close(); |
| // Events |
| c = mContentResolver.query(Events.CONTENT_URI, null, null, null, null); |
| c.close(); |
| // Instances |
| uri = Uri.withAppendedPath(Instances.CONTENT_URI, "0/1"); |
| c = mContentResolver.query(uri, null, null, null, null); |
| c.close(); |
| // Attendees |
| c = mContentResolver.query(Attendees.CONTENT_URI, null, null, null, null); |
| c.close(); |
| // Reminders (only REMINDERS_ID currently uses default projection) |
| uri = ContentUris.withAppendedId(Reminders.CONTENT_URI, 0); |
| c = mContentResolver.query(uri, null, null, null, null); |
| c.close(); |
| // CalendarAlerts |
| c = mContentResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI, |
| null, null, null, null); |
| c.close(); |
| // CalendarCache |
| c = mContentResolver.query(CalendarContract.CalendarCache.URI, |
| null, null, null, null); |
| c.close(); |
| // CalendarEntity |
| c = mContentResolver.query(CalendarContract.CalendarEntity.CONTENT_URI, |
| null, null, null, null); |
| c.close(); |
| // EventEntities |
| c = mContentResolver.query(CalendarContract.EventsEntity.CONTENT_URI, |
| null, null, null, null); |
| c.close(); |
| // EventDays |
| uri = Uri.withAppendedPath(CalendarContract.EventDays.CONTENT_URI, "1/2"); |
| c = mContentResolver.query(uri, null, null, null, null); |
| c.close(); |
| // ExtendedProperties |
| c = mContentResolver.query(CalendarContract.ExtendedProperties.CONTENT_URI, |
| null, null, null, null); |
| c.close(); |
| |
| removeAndVerifyCalendar(account, id); |
| } |
| |
| /** |
| * Exercises the EventsEntity class. |
| */ |
| @MediumTest |
| public void testEventsEntityQuery() { |
| String account = "eeq_account"; |
| int seed = 0; |
| |
| // Clean up just in case. |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar. |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create three events. We need to make sure SELF_ATTENDEE_STATUS isn't set, because |
| // that causes the provider to generate an Attendees entry, and that'll throw off |
| // our expected count. |
| ContentValues eventValues; |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.remove(Events.SELF_ATTENDEE_STATUS); |
| long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId1 >= 0); |
| |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.remove(Events.SELF_ATTENDEE_STATUS); |
| long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId2 >= 0); |
| |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.remove(Events.SELF_ATTENDEE_STATUS); |
| long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId3 >= 0); |
| |
| /* |
| * Add some attendees, reminders, and extended properties. |
| */ |
| Uri uri, syncUri; |
| |
| syncUri = asSyncAdapter(Reminders.CONTENT_URI, account, CTS_TEST_TYPE); |
| ContentValues remValues = new ContentValues(); |
| remValues.put(Reminders.EVENT_ID, eventId1); |
| remValues.put(Reminders.MINUTES, 10); |
| remValues.put(Reminders.METHOD, Reminders.METHOD_ALERT); |
| mContentResolver.insert(syncUri, remValues); |
| remValues.put(Reminders.MINUTES, 20); |
| mContentResolver.insert(syncUri, remValues); |
| |
| syncUri = asSyncAdapter(ExtendedProperties.CONTENT_URI, account, CTS_TEST_TYPE); |
| ContentValues extended = new ContentValues(); |
| extended.put(ExtendedProperties.NAME, "foo"); |
| extended.put(ExtendedProperties.VALUE, "bar"); |
| extended.put(ExtendedProperties.EVENT_ID, eventId2); |
| mContentResolver.insert(syncUri, extended); |
| extended.put(ExtendedProperties.EVENT_ID, eventId1); |
| mContentResolver.insert(syncUri, extended); |
| extended.put(ExtendedProperties.NAME, "foo2"); |
| extended.put(ExtendedProperties.VALUE, "bar2"); |
| mContentResolver.insert(syncUri, extended); |
| |
| syncUri = asSyncAdapter(Attendees.CONTENT_URI, account, CTS_TEST_TYPE); |
| ContentValues attendee = new ContentValues(); |
| attendee.put(Attendees.ATTENDEE_NAME, "Joe"); |
| attendee.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account)); |
| attendee.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED); |
| attendee.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED); |
| attendee.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_PERFORMER); |
| attendee.put(Attendees.EVENT_ID, eventId3); |
| mContentResolver.insert(syncUri, attendee); |
| |
| /* |
| * Iterate over all events on our calendar. Peek at a few values to see if they |
| * look reasonable. |
| */ |
| EntityIterator ei = EventsEntity.newEntityIterator( |
| mContentResolver.query(EventsEntity.CONTENT_URI, null, Events.CALENDAR_ID + "=?", |
| new String[] { String.valueOf(calendarId) }, null), |
| mContentResolver); |
| int count = 0; |
| try { |
| while (ei.hasNext()) { |
| Entity entity = ei.next(); |
| ContentValues values = entity.getEntityValues(); |
| ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues(); |
| long eventId = values.getAsLong(Events._ID); |
| if (eventId == eventId1) { |
| // 2 x reminder, 2 x extended properties |
| assertEquals(4, subvalues.size()); |
| } else if (eventId == eventId2) { |
| // Extended properties |
| assertEquals(1, subvalues.size()); |
| ContentValues subContentValues = subvalues.get(0).values; |
| String name = subContentValues.getAsString( |
| CalendarContract.ExtendedProperties.NAME); |
| String value = subContentValues.getAsString( |
| CalendarContract.ExtendedProperties.VALUE); |
| assertEquals("foo", name); |
| assertEquals("bar", value); |
| } else if (eventId == eventId3) { |
| // Attendees |
| assertEquals(1, subvalues.size()); |
| } else { |
| fail("should not be here"); |
| } |
| count++; |
| } |
| assertEquals(3, count); |
| } finally { |
| ei.close(); |
| } |
| |
| // Confirm that querying for a single event yields a single event. |
| ei = EventsEntity.newEntityIterator( |
| mContentResolver.query(EventsEntity.CONTENT_URI, null, SQL_WHERE_ID, |
| new String[] { String.valueOf(eventId3) }, null), |
| mContentResolver); |
| try { |
| count = 0; |
| while (ei.hasNext()) { |
| Entity entity = ei.next(); |
| count++; |
| } |
| assertEquals(1, count); |
| } finally { |
| ei.close(); |
| } |
| |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Exercises the CalendarEntity class. |
| */ |
| @MediumTest |
| public void testCalendarEntityQuery() { |
| String account1 = "ceq1_account"; |
| String account2 = "ceq2_account"; |
| String account3 = "ceq3_account"; |
| int seed = 0; |
| |
| // Clean up just in case. |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account1); |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account2); |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account3); |
| |
| // Create calendars. |
| long calendarId1 = createAndVerifyCalendar(account1, seed++, null); |
| long calendarId2 = createAndVerifyCalendar(account2, seed++, null); |
| long calendarId3 = createAndVerifyCalendar(account3, seed++, null); |
| |
| EntityIterator ei = CalendarEntity.newEntityIterator( |
| mContentResolver.query(CalendarEntity.CONTENT_URI, null, |
| Calendars._ID + "=? OR " + Calendars._ID + "=? OR " + Calendars._ID + "=?", |
| new String[] { String.valueOf(calendarId1), String.valueOf(calendarId2), |
| String.valueOf(calendarId3) }, |
| null)); |
| |
| try { |
| int count = 0; |
| while (ei.hasNext()) { |
| Entity entity = ei.next(); |
| count++; |
| } |
| assertEquals(3, count); |
| } finally { |
| ei.close(); |
| } |
| |
| removeAndVerifyCalendar(account1, calendarId1); |
| removeAndVerifyCalendar(account2, calendarId2); |
| removeAndVerifyCalendar(account3, calendarId3); |
| } |
| |
| /** |
| * Tests creation and manipulation of Attendees. |
| */ |
| @MediumTest |
| public void testAttendees() { |
| String account = "att_account"; |
| int seed = 0; |
| |
| // Clean up just in case. |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar. |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create two events, one with a value set for SELF_ATTENDEE_STATUS, one without. |
| ContentValues eventValues; |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.put(Events.SELF_ATTENDEE_STATUS, Events.STATUS_TENTATIVE); |
| long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId1 >= 0); |
| |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.remove(Events.SELF_ATTENDEE_STATUS); |
| long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId2 >= 0); |
| |
| /* |
| * Add some attendees to the first event. |
| */ |
| long attId1 = AttendeeHelper.addAttendee(mContentResolver, eventId1, |
| "Alice", |
| "alice@example.com", |
| Attendees.ATTENDEE_STATUS_TENTATIVE, |
| Attendees.RELATIONSHIP_ATTENDEE, |
| Attendees.TYPE_REQUIRED); |
| long attId2 = AttendeeHelper.addAttendee(mContentResolver, eventId1, |
| "Betty", |
| "betty@example.com", |
| Attendees.ATTENDEE_STATUS_DECLINED, |
| Attendees.RELATIONSHIP_ATTENDEE, |
| Attendees.TYPE_NONE); |
| long attId3 = AttendeeHelper.addAttendee(mContentResolver, eventId1, |
| "Carol", |
| "carol@example.com", |
| Attendees.ATTENDEE_STATUS_DECLINED, |
| Attendees.RELATIONSHIP_ATTENDEE, |
| Attendees.TYPE_OPTIONAL); |
| |
| /* |
| * Find the event1 "self" attendee entry. |
| */ |
| Cursor cursor = AttendeeHelper.findAttendeesByEmail(mContentResolver, eventId1, |
| CalendarHelper.generateCalendarOwnerEmail(account)); |
| try { |
| assertEquals(1, cursor.getCount()); |
| //DatabaseUtils.dumpCursor(cursor); |
| |
| cursor.moveToFirst(); |
| long id = cursor.getLong(AttendeeHelper.ATTENDEES_ID_INDEX); |
| |
| /* |
| * Update the status field. The provider should automatically propagate the result. |
| */ |
| ContentValues update = new ContentValues(); |
| Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, id); |
| |
| update.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED); |
| int count = mContentResolver.update(uri, update, null, null); |
| assertEquals(1, count); |
| |
| int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId1); |
| assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status); |
| |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| /* |
| * Do a bulk update of all Attendees for this event, changing any Attendee with status |
| * "declined" to "invited". |
| */ |
| ContentValues bulkUpdate = new ContentValues(); |
| bulkUpdate.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_INVITED); |
| |
| int count = mContentResolver.update(Attendees.CONTENT_URI, bulkUpdate, |
| Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_STATUS + "=?", |
| new String[] { |
| String.valueOf(eventId1), String.valueOf(Attendees.ATTENDEE_STATUS_DECLINED) |
| }); |
| assertEquals(2, count); |
| |
| /* |
| * Add a new, non-self attendee to the second event. |
| */ |
| long attId4 = AttendeeHelper.addAttendee(mContentResolver, eventId2, |
| "Diana", |
| "diana@example.com", |
| Attendees.ATTENDEE_STATUS_ACCEPTED, |
| Attendees.RELATIONSHIP_ATTENDEE, |
| Attendees.TYPE_REQUIRED); |
| |
| /* |
| * Confirm that the selfAttendeeStatus on the second event has the default value. |
| */ |
| int status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2); |
| assertEquals(Attendees.ATTENDEE_STATUS_NONE, status); |
| |
| /* |
| * Create a new "self" attendee in the second event by updating the email address to |
| * match that of the calendar owner. |
| */ |
| ContentValues newSelf = new ContentValues(); |
| newSelf.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account)); |
| count = mContentResolver.update(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4), |
| newSelf, null, null); |
| assertEquals(1, count); |
| |
| /* |
| * Confirm that the event's selfAttendeeStatus has been updated. |
| */ |
| status = EventHelper.lookupSelfAttendeeStatus(mContentResolver, eventId2); |
| assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, status); |
| |
| /* |
| * TODO: (these are unexpected usage patterns) |
| * - Update an Attendee's status and event_id to move it to a different event, and |
| * confirm that the selfAttendeeStatus in the destination event is updated (rather |
| * than that of the source event). |
| * - Create two Attendees with email addresses that match "self" but have different |
| * values for "status". Delete one and confirm that selfAttendeeStatus is changed |
| * to that of the remaining Attendee. (There is no defined behavior for |
| * selfAttendeeStatus when there are multiple matching Attendees.) |
| */ |
| |
| /* |
| * Test deletion, singly by ID and in bulk. |
| */ |
| count = mContentResolver.delete(ContentUris.withAppendedId(Attendees.CONTENT_URI, attId4), |
| null, null); |
| assertEquals(1, count); |
| |
| count = mContentResolver.delete(Attendees.CONTENT_URI, Attendees.EVENT_ID + "=?", |
| new String[] { String.valueOf(eventId1) }); |
| assertEquals(4, count); // 3 we created + 1 auto-added by the provider |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests creation and manipulation of Reminders. |
| */ |
| @MediumTest |
| public void testReminders() { |
| String account = "rem_account"; |
| int seed = 0; |
| |
| // Clean up just in case. |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar. |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create events. |
| ContentValues eventValues; |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId1 >= 0); |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId2 >= 0); |
| |
| // No reminders, hasAlarm should be zero. |
| int hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); |
| assertEquals(0, hasAlarm); |
| hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2); |
| assertEquals(0, hasAlarm); |
| |
| /* |
| * Add some reminders. |
| */ |
| long remId1 = ReminderHelper.addReminder(mContentResolver, eventId1, |
| 10, Reminders.METHOD_DEFAULT); |
| long remId2 = ReminderHelper.addReminder(mContentResolver, eventId1, |
| 15, Reminders.METHOD_ALERT); |
| long remId3 = ReminderHelper.addReminder(mContentResolver, eventId1, |
| 20, Reminders.METHOD_SMS); // SMS isn't allowed for this calendar |
| |
| // Should have been set to 1 by provider. |
| hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); |
| assertEquals(1, hasAlarm); |
| |
| // Add a reminder to event2. |
| ReminderHelper.addReminder(mContentResolver, eventId2, |
| 20, Reminders.METHOD_DEFAULT); |
| hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2); |
| assertEquals(1, hasAlarm); |
| |
| |
| /* |
| * Check the entries. |
| */ |
| Cursor cursor = ReminderHelper.findRemindersByEventId(mContentResolver, eventId1); |
| try { |
| assertEquals(3, cursor.getCount()); |
| //DatabaseUtils.dumpCursor(cursor); |
| |
| while (cursor.moveToNext()) { |
| int minutes = cursor.getInt(ReminderHelper.REMINDERS_MINUTES_INDEX); |
| int method = cursor.getInt(ReminderHelper.REMINDERS_METHOD_INDEX); |
| switch (minutes) { |
| case 10: |
| assertEquals(Reminders.METHOD_DEFAULT, method); |
| break; |
| case 15: |
| assertEquals(Reminders.METHOD_ALERT, method); |
| break; |
| case 20: |
| assertEquals(Reminders.METHOD_SMS, method); |
| break; |
| default: |
| fail("unexpected minutes " + minutes); |
| break; |
| } |
| } |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| /* |
| * Use the bulk update feature to change all METHOD_DEFAULT to METHOD_EMAIL. To make |
| * this more interesting we first change remId3 to METHOD_DEFAULT. |
| */ |
| int count; |
| ContentValues newValues = new ContentValues(); |
| newValues.put(Reminders.METHOD, Reminders.METHOD_DEFAULT); |
| count = mContentResolver.update(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId3), |
| newValues, null, null); |
| assertEquals(1, count); |
| |
| newValues.put(Reminders.METHOD, Reminders.METHOD_EMAIL); |
| count = mContentResolver.update(Reminders.CONTENT_URI, newValues, |
| Reminders.EVENT_ID + "=? AND " + Reminders.METHOD + "=?", |
| new String[] { |
| String.valueOf(eventId1), String.valueOf(Reminders.METHOD_DEFAULT) |
| }); |
| assertEquals(2, count); |
| |
| // check it |
| int method = ReminderHelper.lookupMethod(mContentResolver, remId3); |
| assertEquals(Reminders.METHOD_EMAIL, method); |
| |
| /* |
| * Delete some / all reminders and confirm that hasAlarm tracks it. |
| * |
| * You can also remove reminders from an event by updating the event_id column, but |
| * that's defined as producing undefined behavior, so we don't do it here. |
| */ |
| count = mContentResolver.delete(Reminders.CONTENT_URI, |
| Reminders.EVENT_ID + "=? AND " + Reminders.MINUTES + ">=?", |
| new String[] { String.valueOf(eventId1), "15" }); |
| assertEquals(2, count); |
| hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); |
| assertEquals(1, hasAlarm); |
| |
| // Delete all reminders from both events. |
| count = mContentResolver.delete(Reminders.CONTENT_URI, |
| Reminders.EVENT_ID + "=? OR " + Reminders.EVENT_ID + "=?", |
| new String[] { String.valueOf(eventId1), String.valueOf(eventId2) }); |
| assertEquals(2, count); |
| hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId1); |
| assertEquals(0, hasAlarm); |
| hasAlarm = EventHelper.lookupHasAlarm(mContentResolver, eventId2); |
| assertEquals(0, hasAlarm); |
| |
| /* |
| * Add a couple of reminders and then delete one with the by-ID URI. |
| */ |
| long remId4 = ReminderHelper.addReminder(mContentResolver, eventId1, |
| 10, Reminders.METHOD_EMAIL); |
| long remId5 = ReminderHelper.addReminder(mContentResolver, eventId1, |
| 15, Reminders.METHOD_EMAIL); |
| count = mContentResolver.delete(ContentUris.withAppendedId(Reminders.CONTENT_URI, remId4), |
| null, null); |
| assertEquals(1, count); |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * A listener for the EVENT_REMINDER broadcast that is expected to be fired by the |
| * provider at the reminder time. |
| */ |
| public class MockReminderReceiver extends BroadcastReceiver { |
| public boolean received = false; |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| final String action = intent.getAction(); |
| if (action.equals(CalendarContract.ACTION_EVENT_REMINDER)) { |
| received = true; |
| } |
| } |
| } |
| |
| /** |
| * Test that reminders result in the expected broadcast at reminder time. |
| */ |
| public void testRemindersAlarm() throws Exception { |
| // Setup: register a mock listener for the broadcast we expect to fire at the |
| // reminder time. |
| final MockReminderReceiver reminderReceiver = new MockReminderReceiver(); |
| IntentFilter filter = new IntentFilter(CalendarContract.ACTION_EVENT_REMINDER); |
| filter.addDataScheme("content"); |
| getInstrumentation().getTargetContext().registerReceiver(reminderReceiver, filter); |
| |
| // Clean up just in case. |
| String account = "rem_account"; |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar. Use '1' as seed as this sets the VISIBLE field to 1. |
| // The calendar must be visible for its notifications to occur. |
| long calendarId = createAndVerifyCalendar(account, 1, null); |
| |
| // Create event for 15 min in the past, with a 10 min reminder, so that it will |
| // trigger immediately. |
| ContentValues eventValues; |
| int seed = 0; |
| long now = System.currentTimeMillis(); |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.put(Events.DTSTART, now - DateUtils.MINUTE_IN_MILLIS * 15); |
| eventValues.put(Events.DTEND, now + DateUtils.HOUR_IN_MILLIS); |
| long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId >= 0); |
| ReminderHelper.addReminder(mContentResolver, eventId, 10, Reminders.METHOD_ALERT); |
| |
| // Confirm that the EVENT_REMINDER broadcast was fired by the provider. |
| new PollingCheck(POLLING_TIMEOUT) { |
| @Override |
| protected boolean check() { |
| return reminderReceiver.received; |
| } |
| }.run(); |
| assertTrue(reminderReceiver.received); |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| @MediumTest |
| public void testColorWriteRequirements() { |
| String account = "colw_account"; |
| String account2 = "colw2_account"; |
| int seed = 0; |
| Uri uri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE); |
| Uri uri2 = asSyncAdapter(Colors.CONTENT_URI, account2, CTS_TEST_TYPE); |
| |
| // Clean up just in case |
| ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); |
| |
| ContentValues colorValues = new ContentValues(); |
| // Account name/type must be in the query params, so may be left |
| // out here |
| colorValues.put(Colors.DATA, "0"); |
| colorValues.put(Colors.COLOR_KEY, "1"); |
| colorValues.put(Colors.COLOR_TYPE, 0); |
| colorValues.put(Colors.COLOR, 0xff000000); |
| |
| // Verify only a sync adapter can write to Colors |
| try { |
| mContentResolver.insert(Colors.CONTENT_URI, colorValues); |
| fail("Should not allow non-sync adapter to insert colors"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| // Verify everything except DATA is required |
| ContentValues testVals = new ContentValues(colorValues); |
| for (String key : colorValues.keySet()) { |
| |
| testVals.remove(key); |
| try { |
| Uri colUri = mContentResolver.insert(uri, testVals); |
| if (!TextUtils.equals(key, Colors.DATA)) { |
| // The DATA field is allowed to be empty. |
| fail("Should not allow color creation without " + key); |
| } |
| ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| } catch (IllegalArgumentException e) { |
| if (TextUtils.equals(key, Colors.DATA)) { |
| // The DATA field is allowed to be empty. |
| fail("Should allow color creation without " + key); |
| } |
| } |
| testVals.put(key, colorValues.getAsString(key)); |
| } |
| |
| // Verify writing a color works |
| Uri col1 = mContentResolver.insert(uri, colorValues); |
| |
| // Verify adding the same color fails |
| try { |
| mContentResolver.insert(uri, colorValues); |
| fail("Should not allow adding the same color twice"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| // Verify specifying a different account than the query params doesn't work |
| colorValues.put(Colors.ACCOUNT_NAME, account2); |
| try { |
| mContentResolver.insert(uri, colorValues); |
| fail("Should use the account from the query params, not the values."); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| // Verify adding a color to a different account works |
| Uri col2 = mContentResolver.insert(uri2, colorValues); |
| |
| // And a different index on the same account |
| colorValues.put(Colors.COLOR_KEY, "2"); |
| Uri col3 = mContentResolver.insert(uri2, colorValues); |
| |
| // Verify that all three colors are in the table |
| Cursor c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| assertEquals(1, c.getCount()); |
| c.close(); |
| c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); |
| assertEquals(2, c.getCount()); |
| c.close(); |
| |
| // Verify deleting them works |
| ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| ColorHelper.deleteColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); |
| |
| c = ColorHelper.findColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| assertEquals(0, c.getCount()); |
| c.close(); |
| c = ColorHelper.findColorsByAccount(mContentResolver, account2, CTS_TEST_TYPE); |
| assertEquals(0, c.getCount()); |
| c.close(); |
| } |
| |
| /** |
| * Tests Colors interaction with the Calendars table. |
| */ |
| @MediumTest |
| public void testCalendarColors() { |
| String account = "cc_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| |
| // Test inserting a calendar with an invalid color index |
| ContentValues cv = CalendarHelper.getNewCalendarValues(account, seed++); |
| cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex"); |
| Uri calSyncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE); |
| Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE); |
| |
| try { |
| Uri uri = mContentResolver.insert(calSyncUri, cv); |
| fail("Should not allow insertion of invalid color index into Calendars"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| // Test updating a calendar with an invalid color index |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| cv.clear(); |
| cv.put(Calendars.CALENDAR_COLOR_KEY, "badIndex2"); |
| Uri calendarUri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId); |
| try { |
| mContentResolver.update(calendarUri, cv, null, null); |
| fail("Should not allow update of invalid color index into Calendars"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE)); |
| |
| // Test that inserting a valid color index works |
| cv = CalendarHelper.getNewCalendarValues(account, seed++); |
| cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]); |
| |
| Uri uri = mContentResolver.insert(calSyncUri, cv); |
| long calendarId2 = ContentUris.parseId(uri); |
| assertTrue(calendarId2 >= 0); |
| // And updates the calendar's color to the one in the table |
| cv.put(Calendars.CALENDAR_COLOR, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]); |
| verifyCalendar(account, cv, calendarId2, 2); |
| |
| // Test that updating a valid color index also updates the color in a |
| // calendar |
| cv.clear(); |
| cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]); |
| mContentResolver.update(calendarUri, cv, null, null); |
| Cursor c = mContentResolver.query(calendarUri, |
| new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR }, |
| null, null, null); |
| try { |
| c.moveToFirst(); |
| String index = c.getString(0); |
| int color = c.getInt(1); |
| assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_0]); |
| assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0]); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| // And clearing it doesn't change the color |
| cv.put(Calendars.CALENDAR_COLOR_KEY, (String) null); |
| mContentResolver.update(calendarUri, cv, null, null); |
| c = mContentResolver.query(calendarUri, |
| new String[] { Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR }, |
| null, null, null); |
| try { |
| c.moveToFirst(); |
| String index = c.getString(0); |
| int color = c.getInt(1); |
| assertEquals(index, null); |
| assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.C_COLOR_0], color); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| // Test that setting a calendar color to an event color fails |
| cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0]); |
| try { |
| mContentResolver.update(calendarUri, cv, null, null); |
| fail("Should not allow a calendar to use an event color"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| // Test that you can't remove a color that is referenced by a calendar |
| cv.put(Calendars.CALENDAR_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3]); |
| mContentResolver.update(calendarUri, cv, null, null); |
| |
| try { |
| mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX, |
| new String[] { |
| account, CTS_TEST_TYPE, |
| ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_3] |
| }); |
| fail("Should not allow deleting referenced color"); |
| } catch (UnsupportedOperationException e) { |
| // WAI |
| } |
| |
| // Clean up |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| } |
| |
| /** |
| * Tests Colors interaction with the Events table. |
| */ |
| @MediumTest |
| public void testEventColors() { |
| String account = "ec_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| |
| // Test inserting an event with an invalid color index |
| long cal_id = createAndVerifyCalendar(account, seed++, null); |
| |
| Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, account, CTS_TEST_TYPE); |
| |
| ContentValues ev = EventHelper.getNewEventValues(account, seed++, cal_id, false); |
| ev.put(Events.EVENT_COLOR_KEY, "badIndex"); |
| |
| try { |
| Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev); |
| fail("Should not allow insertion of invalid color index into Events"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| // Test updating an event with an invalid color index fails |
| long event_id = createAndVerifyEvent(account, seed++, cal_id, false, null); |
| ev.clear(); |
| ev.put(Events.EVENT_COLOR_KEY, "badIndex2"); |
| Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, event_id); |
| try { |
| mContentResolver.update(eventUri, ev, null, null); |
| fail("Should not allow update of invalid color index into Events"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| assertTrue(ColorHelper.addDefaultColorsToAccount(mContentResolver, account, CTS_TEST_TYPE)); |
| |
| // Test that inserting a valid color index works |
| ev = EventHelper.getNewEventValues(account, seed++, cal_id, false); |
| final String defaultColorIndex = ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_0]; |
| ev.put(Events.EVENT_COLOR_KEY, defaultColorIndex); |
| |
| Uri uri = mContentResolver.insert(Events.CONTENT_URI, ev); |
| long eventId2 = ContentUris.parseId(uri); |
| assertTrue(eventId2 >= 0); |
| // And updates the event's color to the one in the table |
| final int expectedColor = ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_0]; |
| ev.put(Events.EVENT_COLOR, expectedColor); |
| verifyEvent(ev, eventId2); |
| |
| // Test that event iterator has COLOR columns |
| final EntityIterator iterator = EventsEntity.newEntityIterator(mContentResolver.query( |
| ContentUris.withAppendedId(EventsEntity.CONTENT_URI, eventId2), |
| null, null, null, null), mContentResolver); |
| assertTrue("Empty Iterator", iterator.hasNext()); |
| final Entity entity = iterator.next(); |
| final ContentValues values = entity.getEntityValues(); |
| assertTrue("Missing EVENT_COLOR", values.containsKey(EventsEntity.EVENT_COLOR)); |
| assertEquals("Wrong EVENT_COLOR", |
| expectedColor, |
| (int) values.getAsInteger(EventsEntity.EVENT_COLOR)); |
| assertTrue("Missing EVENT_COLOR_KEY", values.containsKey(EventsEntity.EVENT_COLOR_KEY)); |
| assertEquals("Wrong EVENT_COLOR_KEY", |
| defaultColorIndex, |
| values.getAsString(EventsEntity.EVENT_COLOR_KEY)); |
| iterator.close(); |
| |
| // Test that updating a valid color index also updates the color in an |
| // event |
| ev.clear(); |
| ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]); |
| mContentResolver.update(eventUri, ev, null, null); |
| Cursor c = mContentResolver.query(eventUri, new String[] { |
| Events.EVENT_COLOR_KEY, Events.EVENT_COLOR |
| }, null, null, null); |
| try { |
| c.moveToFirst(); |
| String index = c.getString(0); |
| int color = c.getInt(1); |
| assertEquals(index, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]); |
| assertEquals(color, ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1]); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| // And clearing it doesn't change the color |
| ev.put(Events.EVENT_COLOR_KEY, (String) null); |
| mContentResolver.update(eventUri, ev, null, null); |
| c = mContentResolver.query(eventUri, new String[] { |
| Events.EVENT_COLOR_KEY, Events.EVENT_COLOR |
| }, null, null, null); |
| try { |
| c.moveToFirst(); |
| String index = c.getString(0); |
| int color = c.getInt(1); |
| assertEquals(index, null); |
| assertEquals(ColorHelper.DEFAULT_COLORS[ColorHelper.E_COLOR_1], color); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| // Test that setting an event color to a calendar color fails |
| ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.C_COLOR_2]); |
| try { |
| mContentResolver.update(eventUri, ev, null, null); |
| fail("Should not allow an event to use a calendar color"); |
| } catch (IllegalArgumentException e) { |
| // WAI |
| } |
| |
| // Test that you can't remove a color that is referenced by an event |
| ev.put(Events.EVENT_COLOR_KEY, ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1]); |
| mContentResolver.update(eventUri, ev, null, null); |
| try { |
| mContentResolver.delete(colSyncUri, ColorHelper.WHERE_COLOR_ACCOUNT_AND_INDEX, |
| new String[] { |
| account, CTS_TEST_TYPE, |
| ColorHelper.DEFAULT_INDICES[ColorHelper.E_COLOR_1] |
| }); |
| fail("Should not allow deleting referenced color"); |
| } catch (UnsupportedOperationException e) { |
| // WAI |
| } |
| |
| // TODO test colors with exceptions |
| |
| // Clean up |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| ColorHelper.deleteColorsByAccount(mContentResolver, account, CTS_TEST_TYPE); |
| } |
| |
| /** |
| * Tests creation and manipulation of ExtendedProperties. |
| */ |
| @MediumTest |
| public void testExtendedProperties() { |
| String account = "ep_account"; |
| int seed = 0; |
| |
| // Clean up just in case. |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar. |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create events. |
| ContentValues eventValues; |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId1 >= 0); |
| |
| /* |
| * Add some extended properties. |
| */ |
| long epId1 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account, |
| eventId1, "first", "Jeffrey"); |
| long epId2 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account, |
| eventId1, "last", "Lebowski"); |
| long epId3 = ExtendedPropertiesHelper.addExtendedProperty(mContentResolver, account, |
| eventId1, "title", "Dude"); |
| |
| /* |
| * Spot-check a couple of entries. |
| */ |
| Cursor cursor = ExtendedPropertiesHelper.findExtendedPropertiesByEventId(mContentResolver, |
| eventId1); |
| try { |
| assertEquals(3, cursor.getCount()); |
| //DatabaseUtils.dumpCursor(cursor); |
| |
| while (cursor.moveToNext()) { |
| String name = |
| cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_NAME_INDEX); |
| String value = |
| cursor.getString(ExtendedPropertiesHelper.EXTENDED_PROPERTIES_VALUE_INDEX); |
| |
| if (name.equals("last")) { |
| assertEquals("Lebowski", value); |
| } |
| } |
| |
| String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1, |
| "title"); |
| assertEquals("Dude", title); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| // Update the title. Must be done as a sync adapter. |
| ContentValues newValues = new ContentValues(); |
| newValues.put(ExtendedProperties.VALUE, "Big"); |
| Uri uri = ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, epId3); |
| uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); |
| int count = mContentResolver.update(uri, newValues, null, null); |
| assertEquals(1, count); |
| |
| // check it |
| String title = ExtendedPropertiesHelper.lookupValueByName(mContentResolver, eventId1, |
| "title"); |
| assertEquals("Big", title); |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| private class CalendarEventHelper { |
| |
| private long mCalendarId; |
| private String mAccount; |
| private int mSeed; |
| |
| public CalendarEventHelper(String account, int seed) { |
| mAccount = account; |
| mSeed = seed; |
| ContentValues values = CalendarHelper.getNewCalendarValues(account, seed); |
| mCalendarId = createAndVerifyCalendar(account, seed++, values); |
| } |
| |
| public ContentValues addEvent(String timeString, int timeZoneIndex, long duration) { |
| long event1Start = timeInMillis(timeString, timeZoneIndex); |
| ContentValues eventValues; |
| eventValues = EventHelper.getNewEventValues(mAccount, mSeed++, mCalendarId, true); |
| eventValues.put(Events.DESCRIPTION, timeString); |
| eventValues.put(Events.DTSTART, event1Start); |
| eventValues.put(Events.DTEND, event1Start + duration); |
| eventValues.put(Events.EVENT_TIMEZONE, TIME_ZONES[timeZoneIndex]); |
| long eventId = createAndVerifyEvent(mAccount, mSeed, mCalendarId, true, eventValues); |
| assertTrue(eventId >= 0); |
| return eventValues; |
| } |
| |
| public long getCalendarId() { |
| return mCalendarId; |
| } |
| } |
| |
| /** |
| * Test query to retrieve instances within a certain time interval. |
| */ |
| public void testWhenByDayQuery() { |
| String account = "cser_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create a calendar |
| CalendarEventHelper helper = new CalendarEventHelper(account, seed); |
| |
| // Add events to the calendar--the first two in the queried range |
| List<ContentValues> eventsWithinRange = new ArrayList<ContentValues>(); |
| |
| ContentValues values = helper.addEvent("2009-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS); |
| eventsWithinRange.add(values); |
| |
| values = helper.addEvent("2010-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS); |
| eventsWithinRange.add(values); |
| |
| helper.addEvent("2011-10-01T08:00:00", 0, DateUtils.HOUR_IN_MILLIS); |
| |
| // Prepare the start time and end time of the range to query |
| String startTime = "2009-01-01T00:00:00"; |
| String endTime = "2011-01-01T00:00:00"; |
| int julianStart = getJulianDay(startTime, 0); |
| int julianEnd = getJulianDay(endTime, 0); |
| Uri uri = Uri.withAppendedPath( |
| CalendarContract.Instances.CONTENT_BY_DAY_URI, julianStart + "/" + julianEnd); |
| |
| // Query the range, sorting by event start time |
| Cursor c = mContentResolver.query(uri, null, Instances.CALENDAR_ID + "=" |
| + helper.getCalendarId(), null, Events.DTSTART); |
| |
| // Assert that two events are returned |
| assertEquals(c.getCount(), 2); |
| |
| Set<String> keySet = new HashSet(); |
| keySet.add(Events.DESCRIPTION); |
| keySet.add(Events.DTSTART); |
| keySet.add(Events.DTEND); |
| keySet.add(Events.EVENT_TIMEZONE); |
| |
| // Verify that the contents of those two events match the cursor results |
| verifyContentValuesAgainstCursor(eventsWithinRange, keySet, c); |
| } |
| |
| private void verifyContentValuesAgainstCursor(List<ContentValues> cvs, |
| Set<String> keys, Cursor cursor) { |
| assertEquals(cursor.getCount(), cvs.size()); |
| |
| cursor.moveToFirst(); |
| |
| int i=0; |
| do { |
| ContentValues cv = cvs.get(i); |
| for (String key : keys) { |
| assertEquals(cv.get(key).toString(), |
| cursor.getString(cursor.getColumnIndex(key))); |
| } |
| i++; |
| } while (cursor.moveToNext()); |
| |
| cursor.close(); |
| } |
| |
| private long timeInMillis(String timeString, int timeZoneIndex) { |
| Time startTime = new Time(TIME_ZONES[timeZoneIndex]); |
| startTime.parse3339(timeString); |
| return startTime.toMillis(false); |
| } |
| |
| private int getJulianDay(String timeString, int timeZoneIndex) { |
| Time time = new Time(TIME_ZONES[timeZoneIndex]); |
| time.parse3339(timeString); |
| return Time.getJulianDay(time.toMillis(false), time.gmtoff); |
| } |
| |
| /** |
| * Test instance queries with search parameters. |
| */ |
| @MediumTest |
| public void testInstanceSearch() { |
| String account = "cser_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create a calendar |
| ContentValues values = CalendarHelper.getNewCalendarValues(account, seed); |
| long calendarId = createAndVerifyCalendar(account, seed++, values); |
| |
| String testStart = "2009-10-01T08:00:00"; |
| String timeZone = TIME_ZONES[0]; |
| Time startTime = new Time(timeZone); |
| startTime.parse3339(testStart); |
| long startMillis = startTime.toMillis(false); |
| |
| // Create some events, with different descriptions. (Could also create a single |
| // recurring event and some instance exceptions.) |
| ContentValues eventValues; |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.put(Events.DESCRIPTION, "testevent event-one fiddle"); |
| eventValues.put(Events.DTSTART, startMillis); |
| eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS); |
| eventValues.put(Events.EVENT_TIMEZONE, timeZone); |
| long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId1 >= 0); |
| |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.put(Events.DESCRIPTION, "testevent event-two fuzzle"); |
| eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS); |
| eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2); |
| eventValues.put(Events.EVENT_TIMEZONE, timeZone); |
| long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId2 >= 0); |
| |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.put(Events.DESCRIPTION, "testevent event-three fiddle"); |
| eventValues.put(Events.DTSTART, startMillis + DateUtils.HOUR_IN_MILLIS * 2); |
| eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 3); |
| eventValues.put(Events.EVENT_TIMEZONE, timeZone); |
| long eventId3 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId3 >= 0); |
| |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| eventValues.put(Events.DESCRIPTION, "nontestevent"); |
| eventValues.put(Events.DTSTART, startMillis + (long) (DateUtils.HOUR_IN_MILLIS * 1.5f)); |
| eventValues.put(Events.DTEND, startMillis + DateUtils.HOUR_IN_MILLIS * 2); |
| eventValues.put(Events.EVENT_TIMEZONE, timeZone); |
| long eventId4 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| assertTrue(eventId4 >= 0); |
| |
| String rangeStart = "2009-10-01T00:00:00"; |
| String rangeEnd = "2009-10-01T11:59:59"; |
| String[] projection = new String[] { Instances.BEGIN }; |
| |
| if (false) { |
| Cursor instances = getInstances(timeZone, rangeStart, rangeEnd, projection, |
| new long[] { calendarId }); |
| dumpInstances(instances, timeZone, "all"); |
| instances.close(); |
| } |
| |
| Cursor instances; |
| int count; |
| |
| // Find all matching "testevent". The search matches on partial strings, so this |
| // will also pick up "nontestevent". |
| instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, |
| "testevent", false, projection, new long[] { calendarId }); |
| count = instances.getCount(); |
| instances.close(); |
| assertEquals(4, count); |
| |
| // Find all matching "fiddle" and "event". Set the "by day" flag just to be different. |
| instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, |
| "fiddle event", true, projection, new long[] { calendarId }); |
| count = instances.getCount(); |
| instances.close(); |
| assertEquals(2, count); |
| |
| // Find all matching "fiddle" and "baluchitherium". |
| instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, |
| "baluchitherium fiddle", false, projection, new long[] { calendarId }); |
| count = instances.getCount(); |
| instances.close(); |
| assertEquals(0, count); |
| |
| // Find all matching "event-two". |
| instances = getInstancesSearch(timeZone, rangeStart, rangeEnd, |
| "event-two", false, projection, new long[] { calendarId }); |
| count = instances.getCount(); |
| instances.close(); |
| assertEquals(1, count); |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| @MediumTest |
| public void testCalendarUpdateAsApp() { |
| String account = "cu1_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create a calendar |
| ContentValues values = CalendarHelper.getNewCalendarValues(account, seed); |
| long id = createAndVerifyCalendar(account, seed++, values); |
| |
| Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id); |
| |
| // Update the calendar using the direct Uri |
| ContentValues updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal( |
| values, seed++); |
| assertEquals(1, mContentResolver.update(uri, updateValues, null, null)); |
| |
| verifyCalendar(account, values, id, 1); |
| |
| // Update the calendar using selection + args |
| String selection = Calendars._ID + "=?"; |
| String[] selectionArgs = new String[] { Long.toString(id) }; |
| |
| updateValues = CalendarHelper.getUpdateCalendarValuesWithOriginal(values, seed++); |
| |
| assertEquals(1, mContentResolver.update( |
| Calendars.CONTENT_URI, updateValues, selection, selectionArgs)); |
| |
| verifyCalendar(account, values, id, 1); |
| |
| removeAndVerifyCalendar(account, id); |
| } |
| |
| // TODO test calendar updates as sync adapter |
| |
| /** |
| * Test access to the "syncstate" table. |
| */ |
| @MediumTest |
| public void testSyncState() { |
| String account = "ss_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true); |
| |
| // Create a new sync state entry |
| ContentValues values = SyncStateHelper.getNewSyncStateValues(account); |
| long id = createAndVerifySyncState(account, values); |
| |
| // Look it up with the by-ID URI |
| Cursor c = SyncStateHelper.getSyncStateById(mContentResolver, id); |
| assertNotNull(c); |
| assertEquals(1, c.getCount()); |
| c.close(); |
| |
| // Try to remove it as non-sync-adapter; expected to fail. |
| boolean failed; |
| try { |
| SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, false); |
| failed = false; |
| } catch (IllegalArgumentException iae) { |
| failed = true; |
| } |
| assertTrue("deletion of sync state by app was allowed", failed); |
| |
| // Remove it and verify that it's gone |
| removeAndVerifySyncState(account); |
| } |
| |
| |
| private void verifyEvent(ContentValues values, long eventId) { |
| Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| // Verify |
| Cursor c = mContentResolver |
| .query(eventUri, EventHelper.EVENTS_PROJECTION, null, null, null); |
| assertEquals(1, c.getCount()); |
| assertTrue(c.moveToFirst()); |
| assertEquals(eventId, c.getLong(0)); |
| for (String key : values.keySet()) { |
| int index = c.getColumnIndex(key); |
| assertEquals(key, values.getAsString(key), c.getString(index)); |
| } |
| c.close(); |
| } |
| |
| @MediumTest |
| public void testEventCreationAndDeletion() { |
| String account = "ec1_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar and event |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| ContentValues eventValues = EventHelper |
| .getNewEventValues(account, seed++, calendarId, true); |
| long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| |
| Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| |
| removeAndVerifyEvent(eventUri, eventValues, account); |
| |
| // Attempt to create an event without a calendar ID. |
| ContentValues badValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| badValues.remove(Events.CALENDAR_ID); |
| try { |
| createAndVerifyEvent(account, seed, calendarId, true, badValues); |
| fail("was allowed to create an event without CALENDAR_ID"); |
| } catch (IllegalArgumentException iae) { |
| // expected |
| } |
| |
| // Validation may be relaxed for content providers, so test missing timezone as app. |
| badValues = EventHelper.getNewEventValues(account, seed++, calendarId, false); |
| badValues.remove(Events.EVENT_TIMEZONE); |
| try { |
| createAndVerifyEvent(account, seed, calendarId, false, badValues); |
| fail("was allowed to create an event without EVENT_TIMEZONE"); |
| } catch (IllegalArgumentException iae) { |
| // expected |
| } |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| @MediumTest |
| public void testEventUpdateAsApp() { |
| String account = "em1_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create event as sync adapter |
| ContentValues eventValues = EventHelper |
| .getNewEventValues(account, seed++, calendarId, true); |
| long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| |
| // Update event as app |
| Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| |
| ContentValues updateValues = EventHelper.getUpdateEventValuesWithOriginal(eventValues, |
| seed++, false); |
| assertEquals(1, mContentResolver.update(eventUri, updateValues, null, null)); |
| updateValues.put(Events.DIRTY, 1); // provider should have marked as dirty |
| verifyEvent(updateValues, eventId); |
| |
| // Try nulling out a required value. |
| ContentValues badValues = new ContentValues(updateValues); |
| badValues.putNull(Events.EVENT_TIMEZONE); |
| badValues.remove(Events.DIRTY); |
| try { |
| mContentResolver.update(eventUri, badValues, null, null); |
| fail("was allowed to null out EVENT_TIMEZONE"); |
| } catch (IllegalArgumentException iae) { |
| // good |
| } |
| |
| removeAndVerifyEvent(eventUri, eventValues, account); |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests update of multiple events with a single update call. |
| */ |
| @MediumTest |
| public void testBulkUpdate() { |
| String account = "bup_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| String calendarIdStr = String.valueOf(calendarId); |
| |
| // Create events |
| ContentValues eventValues; |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| long eventId1 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| |
| eventValues = EventHelper.getNewEventValues(account, seed++, calendarId, true); |
| long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| |
| // Update the "description" field in all events in this calendar. |
| String newDescription = "bulk edit"; |
| ContentValues updateValues = new ContentValues(); |
| updateValues.put(Events.DESCRIPTION, newDescription); |
| |
| // Must be sync adapter to do a bulk update. |
| Uri uri = asSyncAdapter(Events.CONTENT_URI, account, CTS_TEST_TYPE); |
| int count = mContentResolver.update(uri, updateValues, SQL_WHERE_CALENDAR_ID, |
| new String[] { calendarIdStr }); |
| |
| // Check to see if the changes went through. |
| Uri eventUri = Events.CONTENT_URI; |
| Cursor c = mContentResolver.query(eventUri, new String[] { Events.DESCRIPTION }, |
| SQL_WHERE_CALENDAR_ID, new String[] { calendarIdStr }, null); |
| assertEquals(2, c.getCount()); |
| while (c.moveToNext()) { |
| assertEquals(newDescription, c.getString(0)); |
| } |
| c.close(); |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests the content provider's enforcement of restrictions on who is allowed to modify |
| * specific columns in a Calendar. |
| * <p> |
| * This attempts to create a new row in the Calendar table, specifying one restricted |
| * column at a time. |
| */ |
| @MediumTest |
| public void testSyncOnlyInsertEnforcement() { |
| // These operations should not succeed, so there should be nothing to clean up after. |
| // TODO: this should be a new event augmented with an illegal column, not a single |
| // column. Otherwise we might be tripping over a "DTSTART must exist" test. |
| ContentValues vals = new ContentValues(); |
| for (int i = 0; i < Calendars.SYNC_WRITABLE_COLUMNS.length; i++) { |
| boolean threw = false; |
| try { |
| vals.clear(); |
| vals.put(Calendars.SYNC_WRITABLE_COLUMNS[i], "1"); |
| mContentResolver.insert(Calendars.CONTENT_URI, vals); |
| } catch (IllegalArgumentException e) { |
| threw = true; |
| } |
| assertTrue("Only sync adapter should be allowed to insert " |
| + Calendars.SYNC_WRITABLE_COLUMNS[i], threw); |
| } |
| } |
| |
| /** |
| * Tests creation of a recurring event. |
| * <p> |
| * This (and the other recurrence tests) uses dates well in the past to reduce the likelihood |
| * of encountering non-test recurring events. (Ideally we would select events associated |
| * with a specific calendar.) With dates well in the past, it's also important to have a |
| * fixed maximum count or end date; otherwise, if the metadata min/max instance values are |
| * large enough, the recurrence recalculation processor could get triggered on an insert or |
| * update and bump up against the 2000-instance limit. |
| * |
| * TODO: need some allDay tests |
| */ |
| @MediumTest |
| public void testRecurrence() { |
| String account = "re_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create recurring event |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, |
| calendarId, true, "2003-08-05T09:00:00", "PT1H", |
| "FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU"); |
| long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| //Log.d(TAG, "+++ basic recurrence eventId is " + eventId); |
| |
| // Check to see if we have the expected number of instances |
| String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); |
| int instanceCount = getInstanceCount(timeZone, "2003-08-05T00:00:00", |
| "2003-08-31T11:59:59", new long[] { calendarId }); |
| if (false) { |
| Cursor instances = getInstances(timeZone, "2003-08-05T00:00:00", "2003-08-31T11:59:59", |
| new String[] { Instances.BEGIN }, new long[] { calendarId }); |
| dumpInstances(instances, timeZone, "initial"); |
| instances.close(); |
| } |
| assertEquals("recurrence instance count", 4, instanceCount); |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests conversion of a regular event to a recurring event. |
| */ |
| @MediumTest |
| public void testConversionToRecurring() { |
| String account = "reconv_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar and event |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| ContentValues eventValues = EventHelper |
| .getNewEventValues(account, seed++, calendarId, true); |
| long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues); |
| |
| long dtstart = eventValues.getAsLong(Events.DTSTART); |
| long dtend = eventValues.getAsLong(Events.DTEND); |
| long durationSecs = (dtend - dtstart) / 1000; |
| |
| ContentValues updateValues = new ContentValues(); |
| updateValues.put(Events.RRULE, "FREQ=WEEKLY"); // recurs forever |
| updateValues.put(Events.DURATION, "P" + durationSecs + "S"); |
| updateValues.putNull(Events.DTEND); |
| |
| // Issue update; do it as app instead of sync adapter to exercise that path. |
| updateAndVerifyEvent(account, calendarId, eventId, false, updateValues); |
| |
| // Make sure LAST_DATE got nulled out by our infinitely repeating sequence. |
| Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| Cursor c = mContentResolver.query(eventUri, new String[] { Events.LAST_DATE }, |
| null, null, null); |
| assertEquals(1, c.getCount()); |
| assertTrue(c.moveToFirst()); |
| assertNull(c.getString(0)); |
| c.close(); |
| |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests creation of a recurring event with single-instance exceptions. |
| */ |
| @MediumTest |
| public void testSingleRecurrenceExceptions() { |
| String account = "rex_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create recurring event. |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, |
| calendarId, true, "1999-03-28T09:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=100"); |
| long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); |
| |
| // Add some attendees and reminders. |
| addAttendees(account, eventId, seed); |
| addReminders(account, eventId, seed); |
| |
| // Select a period that gives us 5 instances. We don't want this to straddle a DST |
| // transition, because we expect the startMinute field to be the same for all |
| // instances, and it's stored as minutes since midnight in the device's time zone. |
| // Things won't be consistent if the event and the device have different ideas about DST. |
| String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); |
| String testStart = "1999-04-18T00:00:00"; |
| String testEnd = "1999-05-16T23:59:59"; |
| String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.END_MINUTE }; |
| |
| Cursor instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "initial"); |
| } |
| |
| assertEquals("initial recurrence instance count", 5, instances.getCount()); |
| |
| /* |
| * Advance the start time of a few instances, and verify. |
| */ |
| |
| // Leave first instance alone. |
| instances.moveToPosition(1); |
| |
| long startMillis; |
| ContentValues excepValues; |
| |
| // Advance the start time of the 2nd instance. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.DTSTART, startMillis + 3600*1000); |
| long excepEventId2 = createAndVerifyException(account, eventId, excepValues, true); |
| instances.moveToNext(); |
| |
| // Advance the start time of the 3rd instance. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.DTSTART, startMillis + 3600*1000*2); |
| long excepEventId3 = createAndVerifyException(account, eventId, excepValues, true); |
| instances.moveToNext(); |
| |
| // Cancel the 4th instance. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.STATUS, Events.STATUS_CANCELED); |
| long excepEventId4 = createAndVerifyException(account, eventId, excepValues, true); |
| instances.moveToNext(); |
| |
| // TODO: try to modify a non-existent instance. |
| |
| instances.close(); |
| |
| // TODO: compare Reminders, Attendees, ExtendedProperties on one of the exception events |
| |
| // Re-query the instances and figure out if they look right. |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "with DTSTART exceptions"); |
| } |
| assertEquals("exceptional recurrence instance count", 4, instances.getCount()); |
| |
| long prevMinute = -1; |
| while (instances.moveToNext()) { |
| // expect the start times for each entry to be different from the previous entry |
| long startMinute = instances.getLong(1); |
| assertTrue("instance start times are different", startMinute != prevMinute); |
| |
| prevMinute = startMinute; |
| } |
| instances.close(); |
| |
| |
| // Delete all of our exceptions, and verify. |
| int deleteCount = 0; |
| deleteCount += deleteException(account, eventId, excepEventId2); |
| deleteCount += deleteException(account, eventId, excepEventId3); |
| deleteCount += deleteException(account, eventId, excepEventId4); |
| assertEquals("events deleted", 3, deleteCount); |
| |
| // Re-query the instances and figure out if they look right. |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "post exception deletion"); |
| } |
| assertEquals("post-exception deletion instance count", 5, instances.getCount()); |
| |
| prevMinute = -1; |
| while (instances.moveToNext()) { |
| // expect the start times for each entry to be the same |
| long startMinute = instances.getLong(1); |
| if (prevMinute != -1) { |
| assertEquals("instance start times are the same", startMinute, prevMinute); |
| } |
| prevMinute = startMinute; |
| } |
| instances.close(); |
| |
| /* |
| * Repeat the test, this time modifying DURATION. |
| */ |
| |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "initial"); |
| } |
| |
| assertEquals("initial recurrence instance count", 5, instances.getCount()); |
| |
| // Leave first instance alone. |
| instances.moveToPosition(1); |
| |
| // Advance the end time of the 2nd instance. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.DURATION, "P" + 3600*2 + "S"); |
| excepEventId2 = createAndVerifyException(account, eventId, excepValues, true); |
| instances.moveToNext(); |
| |
| // Advance the end time of the 3rd instance, and change the self-attendee status. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.DURATION, "P" + 3600*3 + "S"); |
| excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED); |
| excepEventId3 = createAndVerifyException(account, eventId, excepValues, true); |
| instances.moveToNext(); |
| |
| // Advance the start time of the 4th instance, which will also advance the end time. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.DTSTART, startMillis + 3600*1000); |
| excepEventId4 = createAndVerifyException(account, eventId, excepValues, true); |
| instances.moveToNext(); |
| |
| instances.close(); |
| |
| // TODO: make sure the selfAttendeeStatus change took |
| |
| // Re-query the instances and figure out if they look right. |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "with DURATION exceptions"); |
| } |
| assertEquals("exceptional recurrence instance count", 5, instances.getCount()); |
| |
| prevMinute = -1; |
| while (instances.moveToNext()) { |
| // expect the start times for each entry to be different from the previous entry |
| long endMinute = instances.getLong(2); |
| assertTrue("instance end times are different", endMinute != prevMinute); |
| |
| prevMinute = endMinute; |
| } |
| instances.close(); |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests creation of a simple recurrence exception when not pretending to be the sync |
| * adapter. One significant consequence is that we don't set the _sync_id field in the |
| * events, which affects how the provider correlates recurrences and exceptions. |
| */ |
| @MediumTest |
| public void testNonAdapterRecurrenceExceptions() { |
| String account = "rena_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Generate recurring event, with "asSyncAdapter" set to false. |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, |
| calendarId, false, "1991-02-03T12:00:00", "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10"); |
| |
| // Select a period that gives us 3 instances. |
| String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); |
| String testStart = "1991-02-03T00:00:00"; |
| String testEnd = "1991-02-05T23:59:59"; |
| String[] projection = { Instances.BEGIN, Instances.START_MINUTE }; |
| |
| // Expand the bounds of the instances table so we expand future events as they are added. |
| expandInstanceRange(account, calendarId, testStart, testEnd, timeZone); |
| |
| // Create the event in the database. |
| long eventId = createAndVerifyEvent(account, seed++, calendarId, false, eventValues); |
| assertTrue(eventId >= 0); |
| |
| // Add some attendees. |
| addAttendees(account, eventId, seed); |
| |
| Cursor instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "initial"); |
| } |
| assertEquals("initial recurrence instance count", 3, instances.getCount()); |
| |
| /* |
| * Alter the attendee status of the second event. This should cause the instances to |
| * be updated, replacing the previous 2nd instance with the exception instance. If the |
| * code is broken we'll see four instances (because the original instance didn't get |
| * removed) or one instance (because the code correctly deleted all related events but |
| * couldn't correlate the exception with its original recurrence). |
| */ |
| |
| // Leave first instance alone. |
| instances.moveToPosition(1); |
| |
| long startMillis; |
| ContentValues excepValues; |
| |
| // Advance the start time of the 2nd instance. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.SELF_ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_DECLINED); |
| long excepEventId2 = createAndVerifyException(account, eventId, excepValues, false); |
| instances.moveToNext(); |
| |
| instances.close(); |
| |
| // Re-query the instances and figure out if they look right. |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "with exceptions"); |
| } |
| |
| // TODO: this test currently fails due to limitations in the provider |
| //assertEquals("exceptional recurrence instance count", 3, instances.getCount()); |
| |
| instances.close(); |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests insertion of event exceptions before and after a recurring event is created. |
| * <p> |
| * The server may send exceptions down before the event they refer to, so the provider |
| * fills in the originalId of previously-existing exceptions when a recurring event is |
| * inserted. Make sure that works. |
| * <p> |
| * The _sync_id column is only unique with a given calendar. We create events with |
| * identical originalSyncId values in two different calendars to verify that the provider |
| * doesn't update unrelated events. |
| * <p> |
| * We can't use the /exception URI, because that only works if the events are created |
| * in order. |
| */ |
| @MediumTest |
| public void testOutOfOrderRecurrenceExceptions() { |
| String account1 = "roid1_account"; |
| String account2 = "roid2_account"; |
| String startWhen = "1987-08-09T12:00:00"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account1); |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account2); |
| |
| // Create calendars |
| long calendarId1 = createAndVerifyCalendar(account1, seed++, null); |
| long calendarId2 = createAndVerifyCalendar(account2, seed++, null); |
| |
| |
| // Generate base event. |
| ContentValues recurEventValues = EventHelper.getNewRecurringEventValues(account1, seed++, |
| calendarId1, true, startWhen, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10"); |
| |
| // Select a period that gives us 3 instances. |
| String timeZone = recurEventValues.getAsString(Events.EVENT_TIMEZONE); |
| String testStart = "1987-08-09T00:00:00"; |
| String testEnd = "1987-08-11T23:59:59"; |
| String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.EVENT_ID }; |
| |
| /* |
| * We're interested in exploring what the instance expansion code does with the events |
| * as they arrive. It won't do anything at event-creation time unless the instance |
| * range already covers the interesting set of dates, so we need to create and remove |
| * an instance in the same time frame beforehand. |
| */ |
| expandInstanceRange(account1, calendarId1, testStart, testEnd, timeZone); |
| |
| /* |
| * Instances table should be expanded. Do the test. |
| */ |
| |
| final String MAGIC_SYNC_ID = "MagicSyncId"; |
| recurEventValues.put(Events._SYNC_ID, MAGIC_SYNC_ID); |
| |
| // Generate exceptions from base, removing the generated _sync_id and setting the |
| // base event's _sync_id as originalSyncId. |
| ContentValues beforeExcepValues, afterExcepValues, unrelatedExcepValues; |
| beforeExcepValues = new ContentValues(recurEventValues); |
| afterExcepValues = new ContentValues(recurEventValues); |
| unrelatedExcepValues = new ContentValues(recurEventValues); |
| beforeExcepValues.remove(Events._SYNC_ID); |
| afterExcepValues.remove(Events._SYNC_ID); |
| unrelatedExcepValues.remove(Events._SYNC_ID); |
| beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); |
| afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); |
| unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); |
| |
| // Disassociate the "unrelated" exception by moving it to the other calendar. |
| unrelatedExcepValues.put(Events.CALENDAR_ID, calendarId2); |
| |
| // We shift the start time by half an hour, and use the same _sync_id. |
| final long ONE_DAY_MILLIS = 24 * 60 * 60 * 1000; |
| final long ONE_HOUR_MILLIS = 60 * 60 * 1000; |
| final long HALF_HOUR_MILLIS = 30 * 60 * 1000; |
| long dtstartMillis = recurEventValues.getAsLong(Events.DTSTART) + ONE_DAY_MILLIS; |
| beforeExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis); |
| beforeExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS); |
| beforeExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS); |
| beforeExcepValues.remove(Events.DURATION); |
| beforeExcepValues.remove(Events.RRULE); |
| beforeExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); |
| dtstartMillis += ONE_DAY_MILLIS; |
| afterExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis); |
| afterExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS); |
| afterExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS); |
| afterExcepValues.remove(Events.DURATION); |
| afterExcepValues.remove(Events.RRULE); |
| afterExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); |
| dtstartMillis += ONE_DAY_MILLIS; |
| unrelatedExcepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis); |
| unrelatedExcepValues.put(Events.DTSTART, dtstartMillis + HALF_HOUR_MILLIS); |
| unrelatedExcepValues.put(Events.DTEND, dtstartMillis + ONE_HOUR_MILLIS); |
| unrelatedExcepValues.remove(Events.DURATION); |
| unrelatedExcepValues.remove(Events.RRULE); |
| unrelatedExcepValues.put(Events.ORIGINAL_SYNC_ID, MAGIC_SYNC_ID); |
| |
| |
| // Create "before" and "unrelated" exceptions. |
| long beforeEventId = createAndVerifyEvent(account1, seed, calendarId1, true, |
| beforeExcepValues); |
| assertTrue(beforeEventId >= 0); |
| long unrelatedEventId = createAndVerifyEvent(account2, seed, calendarId2, true, |
| unrelatedExcepValues); |
| assertTrue(unrelatedEventId >= 0); |
| |
| // Create recurring event. |
| long recurEventId = createAndVerifyEvent(account1, seed, calendarId1, true, |
| recurEventValues); |
| assertTrue(recurEventId >= 0); |
| |
| // Create "after" exception. |
| long afterEventId = createAndVerifyEvent(account1, seed, calendarId1, true, |
| afterExcepValues); |
| assertTrue(afterEventId >= 0); |
| |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "before=" + beforeEventId + ", unrel=" + unrelatedEventId + |
| ", recur=" + recurEventId + ", after=" + afterEventId); |
| } |
| |
| // Check to see how many instances we get. If the recurrence and the exception don't |
| // get paired up correctly, we'll see too many instances. |
| Cursor instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId1, calendarId2 }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "with exception"); |
| } |
| |
| assertEquals("initial recurrence instance count", 3, instances.getCount()); |
| |
| instances.close(); |
| |
| |
| /* |
| * Now we want to verify that: |
| * - "before" and "after" have an originalId equal to our recurEventId |
| * - "unrelated" has no originalId |
| */ |
| Cursor c = null; |
| try { |
| final String[] PROJECTION = new String[] { Events.ORIGINAL_ID }; |
| Uri eventUri; |
| Long originalId; |
| |
| eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, beforeEventId); |
| c = mContentResolver.query(eventUri, PROJECTION, null, null, null); |
| assertEquals(1, c.getCount()); |
| c.moveToNext(); |
| originalId = c.getLong(0); |
| assertNotNull(originalId); |
| assertEquals(recurEventId, (long) originalId); |
| c.close(); |
| |
| eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, afterEventId); |
| c = mContentResolver.query(eventUri, PROJECTION, null, null, null); |
| assertEquals(1, c.getCount()); |
| c.moveToNext(); |
| originalId = c.getLong(0); |
| assertNotNull(originalId); |
| assertEquals(recurEventId, (long) originalId); |
| c.close(); |
| |
| eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, unrelatedEventId); |
| c = mContentResolver.query(eventUri, PROJECTION, null, null, null); |
| assertEquals(1, c.getCount()); |
| c.moveToNext(); |
| assertNull(c.getString(0)); |
| c.close(); |
| |
| c = null; |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| // delete the calendars |
| removeAndVerifyCalendar(account1, calendarId1); |
| removeAndVerifyCalendar(account2, calendarId2); |
| } |
| |
| /** |
| * Tests exceptions that modify all future instances of a recurring event. |
| */ |
| @MediumTest |
| public void testForwardRecurrenceExceptions() { |
| String account = "refx_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create recurring event |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, |
| calendarId, true, "1999-01-01T06:00:00", "PT1H", "FREQ=WEEKLY;WKST=SU;COUNT=10"); |
| long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); |
| |
| // Add some attendees and reminders. |
| addAttendees(account, eventId, seed++); |
| addReminders(account, eventId, seed++); |
| |
| // Get some instances. |
| String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); |
| String testStart = "1999-01-01T00:00:00"; |
| String testEnd = "1999-01-29T23:59:59"; |
| String[] projection = { Instances.BEGIN, Instances.START_MINUTE }; |
| |
| Cursor instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "initial"); |
| } |
| |
| assertEquals("initial recurrence instance count", 5, instances.getCount()); |
| |
| // Modify starting from 3rd instance. |
| instances.moveToPosition(2); |
| |
| long startMillis; |
| ContentValues excepValues; |
| |
| // Replace with a new recurrence rule. We move the start time an hour later, and cap |
| // it at two instances. |
| startMillis = instances.getLong(0); |
| excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.DTSTART, startMillis + 3600*1000); |
| excepValues.put(Events.RRULE, "FREQ=WEEKLY;COUNT=2;WKST=SU"); |
| long excepEventId = createAndVerifyException(account, eventId, excepValues, true); |
| instances.close(); |
| |
| |
| // Check to see if it took. |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "with new rule"); |
| } |
| |
| assertEquals("count with exception", 4, instances.getCount()); |
| |
| long prevMinute = -1; |
| for (int i = 0; i < 4; i++) { |
| long startMinute; |
| instances.moveToNext(); |
| switch (i) { |
| case 0: |
| startMinute = instances.getLong(1); |
| break; |
| case 1: |
| case 3: |
| startMinute = instances.getLong(1); |
| assertEquals("first/last pairs match", prevMinute, startMinute); |
| break; |
| case 2: |
| startMinute = instances.getLong(1); |
| assertFalse("first two != last two", prevMinute == startMinute); |
| break; |
| default: |
| fail(); |
| startMinute = -1; // make compiler happy |
| break; |
| } |
| |
| prevMinute = startMinute; |
| } |
| instances.close(); |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests exceptions that modify all instances of a recurring event. This is not really an |
| * exception, since it won't create a new event, but supporting it allows us to use the |
| * exception URI without having to determine whether the "start from here" instance is the |
| * very first instance. |
| */ |
| @MediumTest |
| public void testFullRecurrenceUpdate() { |
| String account = "ref_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create recurring event |
| String rrule = "FREQ=DAILY;WKST=MO;COUNT=100"; |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, |
| calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule); |
| long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); |
| //Log.i(TAG, "+++ eventId is " + eventId); |
| |
| // Get some instances. |
| String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE); |
| String testStart = "1997-08-01T00:00:00"; |
| String testEnd = "1997-08-31T23:59:59"; |
| String[] projection = { Instances.BEGIN, Instances.EVENT_LOCATION }; |
| String newLocation = "NEW!"; |
| |
| Cursor instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "initial"); |
| } |
| |
| assertEquals("initial recurrence instance count", 3, instances.getCount()); |
| |
| instances.moveToFirst(); |
| long startMillis = instances.getLong(0); |
| ContentValues excepValues = EventHelper.getNewExceptionValues(startMillis); |
| excepValues.put(Events.RRULE, rrule); // identifies this as an "all future events" excep |
| excepValues.put(Events.EVENT_LOCATION, newLocation); |
| long excepEventId = createAndVerifyException(account, eventId, excepValues, true); |
| instances.close(); |
| |
| // Check results. |
| assertEquals("full update does not create new ID", eventId, excepEventId); |
| |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| assertEquals("post-update instance count", 3, instances.getCount()); |
| while (instances.moveToNext()) { |
| assertEquals("new location", newLocation, instances.getString(1)); |
| } |
| instances.close(); |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| @MediumTest |
| public void testMultiRuleRecurrence() { |
| String account = "multirule_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create recurring event |
| String rrule = "FREQ=DAILY;WKST=MO;COUNT=5\nFREQ=WEEKLY;WKST=SU;COUNT=5"; |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, |
| calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule); |
| long eventId = createAndVerifyEvent(account, seed++, calendarId, true, eventValues); |
| |
| // TODO: once multi-rule RRULEs are fully supported, verify that they work |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Issue bad requests and expect them to get rejected. |
| */ |
| @MediumTest |
| public void testBadRequests() { |
| String account = "neg_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| // Create calendar |
| long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Create recurring event |
| String rrule = "FREQ=OFTEN;WKST=MO"; |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed++, |
| calendarId, true, "1997-08-29T02:14:00", "PT1H", rrule); |
| try { |
| createAndVerifyEvent(account, seed++, calendarId, true, eventValues); |
| fail("Bad recurrence rule should have been rejected"); |
| } catch (IllegalArgumentException iae) { |
| // good |
| } |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Tests correct behavior of Calendars.isPrimary column |
| */ |
| @MediumTest |
| public void testCalendarIsPrimary() { |
| String account = "ec_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| int isPrimary; |
| Cursor cursor; |
| ContentValues values = new ContentValues(); |
| |
| final long calendarId = createAndVerifyCalendar(account, seed++, null); |
| final Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calendarId); |
| |
| // verify when ownerAccount != account_name && isPrimary IS NULL |
| cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null); |
| cursor.moveToFirst(); |
| isPrimary = cursor.getInt(0); |
| cursor.close(); |
| assertEquals("isPrimary should be 0 if ownerAccount != account_name", 0, isPrimary); |
| |
| // verify when ownerAccount == account_name && isPrimary IS NULL |
| values.clear(); |
| values.put(Calendars.OWNER_ACCOUNT, account); |
| mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null); |
| cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null); |
| cursor.moveToFirst(); |
| isPrimary = cursor.getInt(0); |
| cursor.close(); |
| assertEquals("isPrimary should be 1 if ownerAccount == account_name", 1, isPrimary); |
| |
| // verify isPrimary IS NOT NULL |
| values.clear(); |
| values.put(Calendars.IS_PRIMARY, SOME_ARBITRARY_INT); |
| mContentResolver.update(uri, values, null, null); |
| cursor = mContentResolver.query(uri, new String[]{Calendars.IS_PRIMARY}, null, null, null); |
| cursor.moveToFirst(); |
| isPrimary = cursor.getInt(0); |
| cursor.close(); |
| assertEquals("isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isPrimary); |
| |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| } |
| |
| /** |
| * Tests correct behavior of Events.isOrganizer column |
| */ |
| @MediumTest |
| public void testEventsIsOrganizer() { |
| String account = "ec_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| int isOrganizer; |
| Cursor cursor; |
| ContentValues values = new ContentValues(); |
| |
| final long calendarId = createAndVerifyCalendar(account, seed++, null); |
| final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null); |
| final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| |
| // verify when ownerAccount != organizer && isOrganizer IS NULL |
| cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null); |
| cursor.moveToFirst(); |
| isOrganizer = cursor.getInt(0); |
| cursor.close(); |
| assertEquals("isOrganizer should be 0 if ownerAccount != organizer", 0, isOrganizer); |
| |
| // verify when ownerAccount == account_name && isOrganizer IS NULL |
| values.clear(); |
| values.put(Events.ORGANIZER, CalendarHelper.generateCalendarOwnerEmail(account)); |
| mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null); |
| cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null); |
| cursor.moveToFirst(); |
| isOrganizer = cursor.getInt(0); |
| cursor.close(); |
| assertEquals("isOrganizer should be 1 if ownerAccount == organizer", 1, isOrganizer); |
| |
| // verify isOrganizer IS NOT NULL |
| values.clear(); |
| values.put(Events.IS_ORGANIZER, SOME_ARBITRARY_INT); |
| mContentResolver.update(uri, values, null, null); |
| cursor = mContentResolver.query(uri, new String[]{Events.IS_ORGANIZER}, null, null, null); |
| cursor.moveToFirst(); |
| isOrganizer = cursor.getInt(0); |
| cursor.close(); |
| assertEquals( |
| "isPrimary should be the value it was set to", SOME_ARBITRARY_INT, isOrganizer); |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| } |
| |
| /** |
| * Tests correct behavior of Events.uid2445 column |
| */ |
| @MediumTest |
| public void testEventsUid2445() { |
| String account = "ec_account"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| final String uid = "uid_123"; |
| Cursor cursor; |
| ContentValues values = new ContentValues(); |
| final long calendarId = createAndVerifyCalendar(account, seed++, null); |
| final long eventId = createAndVerifyEvent(account, seed, calendarId, true, null); |
| final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| |
| // Verify default is null |
| cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null); |
| cursor.moveToFirst(); |
| assertTrue(cursor.isNull(0)); |
| cursor.close(); |
| |
| // Write column value and read back |
| values.clear(); |
| values.put(Events.UID_2445, uid); |
| mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null); |
| cursor = mContentResolver.query(uri, new String[] {Events.UID_2445}, null, null, null); |
| cursor.moveToFirst(); |
| assertFalse(cursor.isNull(0)); |
| assertEquals("Column uid_2445 has unexpected value.", uid, cursor.getString(0)); |
| |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| } |
| |
| @MediumTest |
| public void testMutatorSetCorrectly() { |
| String account = "ec_account"; |
| String packageName = "com.android.cts.provider"; |
| int seed = 0; |
| |
| // Clean up just in case |
| CalendarHelper.deleteCalendarByAccount(mContentResolver, account); |
| |
| String mutator; |
| Cursor cursor; |
| ContentValues values = new ContentValues(); |
| final long calendarId = createAndVerifyCalendar(account, seed++, null); |
| |
| // Verify mutator is set to the package, via: |
| // Create: |
| final long eventId = createAndVerifyEvent(account, seed, calendarId, false, null); |
| final Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| cursor = mContentResolver.query(uri, new String[] {Events.MUTATORS}, null, null, null); |
| cursor.moveToFirst(); |
| mutator = cursor.getString(0); |
| cursor.close(); |
| assertEquals(packageName, mutator); |
| |
| // Edit: |
| // First clear the mutator column |
| values.clear(); |
| values.putNull(Events.MUTATORS); |
| mContentResolver.update(asSyncAdapter(uri, account, CTS_TEST_TYPE), values, null, null); |
| cursor = mContentResolver.query(uri, new String[] {Events.MUTATORS}, null, null, null); |
| cursor.moveToFirst(); |
| mutator = cursor.getString(0); |
| cursor.close(); |
| assertNull(mutator); |
| // Now edit the event and verify the mutator column |
| values.clear(); |
| values.put(Events.TITLE, "New title"); |
| mContentResolver.update(uri, values, null, null); |
| cursor = mContentResolver.query(uri, new String[] {Events.MUTATORS}, null, null, null); |
| cursor.moveToFirst(); |
| mutator = cursor.getString(0); |
| cursor.close(); |
| assertEquals(packageName, mutator); |
| |
| // Clean up the event |
| assertEquals(1, EventHelper.deleteEventAsSyncAdapter(mContentResolver, uri, account)); |
| |
| // Delete: |
| // First create as sync adapter |
| final long eventId2 = createAndVerifyEvent(account, seed, calendarId, true, null); |
| final Uri uri2 = ContentUris.withAppendedId(Events.CONTENT_URI, eventId2); |
| // Now delete the event and verify |
| values.clear(); |
| values.put(Events.MUTATORS, packageName); |
| removeAndVerifyEvent(uri2, values, account); |
| |
| |
| // delete the calendar |
| removeAndVerifyCalendar(account, calendarId); |
| } |
| |
| /** |
| * Acquires the set of instances that appear between the specified start and end points. |
| * |
| * @param timeZone Time zone to use when parsing startWhen and endWhen |
| * @param startWhen Start date/time, in RFC 3339 format |
| * @param endWhen End date/time, in RFC 3339 format |
| * @param projection Array of desired column names |
| * @return Cursor with instances (caller should close when done) |
| */ |
| private Cursor getInstances(String timeZone, String startWhen, String endWhen, |
| String[] projection, long[] calendarIds) { |
| Time startTime = new Time(timeZone); |
| startTime.parse3339(startWhen); |
| long startMillis = startTime.toMillis(false); |
| |
| Time endTime = new Time(timeZone); |
| endTime.parse3339(endWhen); |
| long endMillis = endTime.toMillis(false); |
| |
| // We want a list of instances that occur between the specified dates. Use the |
| // "instances/when" URI. |
| Uri uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI, |
| startMillis + "/" + endMillis); |
| |
| String where = null; |
| for (int i = 0; i < calendarIds.length; i++) { |
| if (i > 0) { |
| where += " OR "; |
| } else { |
| where = ""; |
| } |
| where += (Instances.CALENDAR_ID + "=" + calendarIds[i]); |
| } |
| Cursor instances = mContentResolver.query(uri, projection, where, null, |
| projection[0] + " ASC"); |
| |
| return instances; |
| } |
| |
| /** |
| * Acquires the set of instances that appear between the specified start and end points |
| * that match the search terms. |
| * |
| * @param timeZone Time zone to use when parsing startWhen and endWhen |
| * @param startWhen Start date/time, in RFC 3339 format |
| * @param endWhen End date/time, in RFC 3339 format |
| * @param search A collection of tokens to search for. The columns searched are |
| * hard-coded in the provider (currently title, description, location, attendee |
| * name, attendee email). |
| * @param searchByDay If set, adjust start/end to calendar day boundaries. |
| * @param projection Array of desired column names |
| * @return Cursor with instances (caller should close when done) |
| */ |
| private Cursor getInstancesSearch(String timeZone, String startWhen, String endWhen, |
| String search, boolean searchByDay, String[] projection, long[] calendarIds) { |
| Time startTime = new Time(timeZone); |
| startTime.parse3339(startWhen); |
| long startMillis = startTime.toMillis(false); |
| |
| Time endTime = new Time(timeZone); |
| endTime.parse3339(endWhen); |
| long endMillis = endTime.toMillis(false); |
| |
| Uri uri; |
| if (searchByDay) { |
| // start/end are Julian day numbers rather than time in milliseconds |
| int julianStart = Time.getJulianDay(startMillis, startTime.gmtoff); |
| int julianEnd = Time.getJulianDay(endMillis, endTime.gmtoff); |
| uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_BY_DAY_URI, |
| julianStart + "/" + julianEnd + "/" + search); |
| } else { |
| uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_SEARCH_URI, |
| startMillis + "/" + endMillis + "/" + search); |
| } |
| |
| String where = null; |
| for (int i = 0; i < calendarIds.length; i++) { |
| if (i > 0) { |
| where += " OR "; |
| } else { |
| where = ""; |
| } |
| where += (Instances.CALENDAR_ID + "=" + calendarIds[i]); |
| } |
| // We want a list of instances that occur between the specified dates and that match |
| // the search terms. |
| |
| Cursor instances = mContentResolver.query(uri, projection, where, null, |
| projection[0] + " ASC"); |
| |
| return instances; |
| } |
| |
| /** debug -- dump instances cursor */ |
| private static void dumpInstances(Cursor instances, String timeZone, String msg) { |
| Log.d(TAG, "Instances (" + msg + ")"); |
| |
| int posn = instances.getPosition(); |
| instances.moveToPosition(-1); |
| |
| //Log.d(TAG, "+++ instances has " + instances.getCount() + " rows, " + |
| // instances.getColumnCount() + " columns"); |
| while (instances.moveToNext()) { |
| long beginMil = instances.getLong(0); |
| Time beginT = new Time(timeZone); |
| beginT.set(beginMil); |
| String logMsg = "--> begin=" + beginT.format3339(false) + " (" + beginMil + ")"; |
| for (int i = 2; i < instances.getColumnCount(); i++) { |
| logMsg += " [" + instances.getString(i) + "]"; |
| } |
| Log.d(TAG, logMsg); |
| } |
| instances.moveToPosition(posn); |
| } |
| |
| |
| /** |
| * Counts the number of instances that appear between the specified start and end times. |
| */ |
| private int getInstanceCount(String timeZone, String startWhen, String endWhen, |
| long[] calendarIds) { |
| Cursor instances = getInstances(timeZone, startWhen, endWhen, |
| new String[] { Instances._ID }, calendarIds); |
| int count = instances.getCount(); |
| instances.close(); |
| return count; |
| } |
| |
| /** |
| * Deletes an event as app and sync adapter which removes it from the db and |
| * verifies after each. |
| * |
| * @param eventUri The uri for the event to delete |
| * @param accountName TODO |
| */ |
| private void removeAndVerifyEvent(Uri eventUri, ContentValues eventValues, String accountName) { |
| // Delete event |
| EventHelper.deleteEvent(mContentResolver, eventUri, eventValues); |
| // Verify |
| verifyEvent(eventValues, ContentUris.parseId(eventUri)); |
| // Delete as sync adapter |
| assertEquals(1, |
| EventHelper.deleteEventAsSyncAdapter(mContentResolver, eventUri, accountName)); |
| // Verify |
| Cursor c = EventHelper.getEventByUri(mContentResolver, eventUri); |
| assertEquals(0, c.getCount()); |
| c.close(); |
| } |
| |
| /** |
| * Creates an event on the given calendar and verifies it. |
| * |
| * @param account |
| * @param seed |
| * @param calendarId |
| * @param asSyncAdapter |
| * @param values optional pre created set of values; will have several new entries added |
| * @return the _id for the new event |
| */ |
| private long createAndVerifyEvent(String account, int seed, long calendarId, |
| boolean asSyncAdapter, ContentValues values) { |
| // Create an event |
| if (values == null) { |
| values = EventHelper.getNewEventValues(account, seed, calendarId, asSyncAdapter); |
| } |
| Uri insertUri = Events.CONTENT_URI; |
| if (asSyncAdapter) { |
| insertUri = asSyncAdapter(insertUri, account, CTS_TEST_TYPE); |
| } |
| Uri uri = mContentResolver.insert(insertUri, values); |
| assertNotNull(uri); |
| |
| // Verify |
| EventHelper.addDefaultReadOnlyValues(values, account, asSyncAdapter); |
| long eventId = ContentUris.parseId(uri); |
| assertTrue(eventId >= 0); |
| |
| verifyEvent(values, eventId); |
| return eventId; |
| } |
| |
| /** |
| * Updates an event, and verifies that the updates took. |
| */ |
| private void updateAndVerifyEvent(String account, long calendarId, long eventId, |
| boolean asSyncAdapter, ContentValues updateValues) { |
| Uri uri = Uri.withAppendedPath(Events.CONTENT_URI, String.valueOf(eventId)); |
| if (asSyncAdapter) { |
| uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); |
| } |
| int count = mContentResolver.update(uri, updateValues, null, null); |
| |
| // Verify |
| assertEquals(1, count); |
| verifyEvent(updateValues, eventId); |
| } |
| |
| /** |
| * Creates an exception to a recurring event, and verifies it. |
| * @param account The account to use. |
| * @param originalEventId The ID of the original event. |
| * @param values Values for the exception; must include originalInstanceTime. |
| * @return The _id for the new event. |
| */ |
| private long createAndVerifyException(String account, long originalEventId, |
| ContentValues values, boolean asSyncAdapter) { |
| // Create the exception |
| Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, |
| String.valueOf(originalEventId)); |
| if (asSyncAdapter) { |
| uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); |
| } |
| Uri resultUri = mContentResolver.insert(uri, values); |
| assertNotNull(resultUri); |
| long eventId = ContentUris.parseId(resultUri); |
| assertTrue(eventId >= 0); |
| return eventId; |
| } |
| |
| /** |
| * Deletes an exception to a recurring event. |
| * @param account The account to use. |
| * @param eventId The ID of the original recurring event. |
| * @param excepId The ID of the exception event. |
| * @return The number of rows deleted. |
| */ |
| private int deleteException(String account, long eventId, long excepId) { |
| Uri uri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, |
| eventId + "/" + excepId); |
| uri = asSyncAdapter(uri, account, CTS_TEST_TYPE); |
| return mContentResolver.delete(uri, null, null); |
| } |
| |
| /** |
| * Add some sample attendees to an event. |
| */ |
| private void addAttendees(String account, long eventId, int seed) { |
| assertTrue(eventId >= 0); |
| AttendeeHelper.addAttendee(mContentResolver, eventId, |
| "Attender" + seed, |
| CalendarHelper.generateCalendarOwnerEmail(account), |
| Attendees.ATTENDEE_STATUS_ACCEPTED, |
| Attendees.RELATIONSHIP_ORGANIZER, |
| Attendees.TYPE_NONE); |
| seed++; |
| |
| AttendeeHelper.addAttendee(mContentResolver, eventId, |
| "Attender" + seed, |
| "attender" + seed + "@example.com", |
| Attendees.ATTENDEE_STATUS_TENTATIVE, |
| Attendees.RELATIONSHIP_NONE, |
| Attendees.TYPE_NONE); |
| } |
| |
| /** |
| * Add some sample reminders to an event. |
| */ |
| private void addReminders(String account, long eventId, int seed) { |
| ReminderHelper.addReminder(mContentResolver, eventId, seed * 5, Reminders.METHOD_ALERT); |
| } |
| |
| /** |
| * Creates and removes an event that covers a specific range of dates. Call this to |
| * cause the provider to expand the CalendarMetaData min/max values to include the range. |
| * Useful when you want to see the provider expand the instances as the events are added. |
| */ |
| private void expandInstanceRange(String account, long calendarId, String testStart, |
| String testEnd, String timeZone) { |
| int seed = 0; |
| |
| // TODO: this should use an UNTIL rule based on testEnd, not a COUNT |
| ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed, |
| calendarId, true, testStart, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=100"); |
| |
| /* |
| * Some of the helper functions modify "eventValues", so we want to make sure we're |
| * passing a copy of anything we want to re-use. |
| */ |
| long eventId = createAndVerifyEvent(account, seed, calendarId, true, |
| new ContentValues(eventValues)); |
| assertTrue(eventId >= 0); |
| |
| String[] projection = { Instances.BEGIN, Instances.START_MINUTE }; |
| Cursor instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "prep-create"); |
| } |
| assertEquals("initial recurrence instance count", 3, instances.getCount()); |
| instances.close(); |
| |
| Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId); |
| removeAndVerifyEvent(eventUri, new ContentValues(eventValues), account); |
| |
| instances = getInstances(timeZone, testStart, testEnd, projection, |
| new long[] { calendarId }); |
| if (DEBUG_RECURRENCE) { |
| dumpInstances(instances, timeZone, "prep-clear"); |
| } |
| assertEquals("initial recurrence instance count", 0, instances.getCount()); |
| instances.close(); |
| |
| } |
| |
| /** |
| * Inserts a new calendar with the given account and seed and verifies it. |
| * |
| * @param account The account to add the calendar to |
| * @param seed A number to use to generate the values |
| * @return the created calendar's id |
| */ |
| private long createAndVerifyCalendar(String account, int seed, ContentValues values) { |
| // Create a calendar |
| if (values == null) { |
| values = CalendarHelper.getNewCalendarValues(account, seed); |
| } |
| Uri syncUri = asSyncAdapter(Calendars.CONTENT_URI, account, CTS_TEST_TYPE); |
| Uri uri = mContentResolver.insert(syncUri, values); |
| long calendarId = ContentUris.parseId(uri); |
| assertTrue(calendarId >= 0); |
| |
| verifyCalendar(account, values, calendarId, 1); |
| return calendarId; |
| } |
| |
| /** |
| * Deletes a given calendar and verifies no calendars remain on that |
| * account. |
| * |
| * @param account |
| * @param id |
| */ |
| private void removeAndVerifyCalendar(String account, long id) { |
| // TODO Add code to delete as app and sync adapter and test both |
| |
| // Delete |
| assertEquals(1, CalendarHelper.deleteCalendarById(mContentResolver, id)); |
| |
| // Verify |
| Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account); |
| assertEquals(0, c.getCount()); |
| c.close(); |
| } |
| |
| /** |
| * Check all the fields of a calendar contained in values + id. |
| * |
| * @param account the account of the calendar |
| * @param values the values to check against the db |
| * @param id the _id of the calendar |
| * @param expectedCount the number of calendars expected on this account |
| */ |
| private void verifyCalendar(String account, ContentValues values, long id, int expectedCount) { |
| // Verify |
| Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account); |
| assertEquals(expectedCount, c.getCount()); |
| assertTrue(c.moveToFirst()); |
| while (c.getLong(0) != id) { |
| assertTrue(c.moveToNext()); |
| } |
| for (String key : values.keySet()) { |
| int index = c.getColumnIndex(key); |
| assertTrue("Key " + key + " not in projection", index >= 0); |
| assertEquals(key, values.getAsString(key), c.getString(index)); |
| } |
| c.close(); |
| } |
| |
| /** |
| * Creates a new _sync_state entry and verifies the contents. |
| */ |
| private long createAndVerifySyncState(String account, ContentValues values) { |
| assertNotNull(values); |
| Uri syncUri = asSyncAdapter(SyncState.CONTENT_URI, account, CTS_TEST_TYPE); |
| Uri uri = mContentResolver.insert(syncUri, values); |
| long syncStateId = ContentUris.parseId(uri); |
| assertTrue(syncStateId >= 0); |
| |
| verifySyncState(account, values, syncStateId); |
| return syncStateId; |
| |
| } |
| |
| /** |
| * Removes the _sync_state entry with the specified id, then verifies that it's gone. |
| */ |
| private void removeAndVerifySyncState(String account) { |
| assertEquals(1, SyncStateHelper.deleteSyncStateByAccount(mContentResolver, account, true)); |
| |
| // Verify |
| Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account); |
| try { |
| assertEquals(0, c.getCount()); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| } |
| |
| /** |
| * Check all the fields of a _sync_state entry contained in values + id. This assumes |
| * a single _sync_state has been created on the given account. |
| */ |
| private void verifySyncState(String account, ContentValues values, long id) { |
| // Verify |
| Cursor c = SyncStateHelper.getSyncStateByAccount(mContentResolver, account); |
| try { |
| assertEquals(1, c.getCount()); |
| assertTrue(c.moveToFirst()); |
| assertEquals(id, c.getLong(0)); |
| for (String key : values.keySet()) { |
| int index = c.getColumnIndex(key); |
| if (key.equals(SyncState.DATA)) { |
| // TODO: can't compare as string, so compare as byte[] |
| } else { |
| assertEquals(key, values.getAsString(key), c.getString(index)); |
| } |
| } |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| } |
| |
| |
| /** |
| * Special version of the test runner that does some remote Emma coverage housekeeping. |
| */ |
| // TODO: find if this is still used and if so convert to AndroidJUnitRunner framework |
| public static class CalendarEmmaTestRunner extends android.test.InstrumentationTestRunner { |
| private static final Uri EMMA_CONTENT_URI = |
| Uri.parse("content://" + CalendarContract.AUTHORITY + "/emma"); |
| private ContentResolver mContentResolver; |
| |
| @Override |
| public void onStart() { |
| mContentResolver = getTargetContext().getContentResolver(); |
| |
| ContentValues values = new ContentValues(); |
| values.put("cmd", "start"); |
| mContentResolver.insert(EMMA_CONTENT_URI, values); |
| |
| super.onStart(); |
| } |
| |
| @Override |
| public void finish(int resultCode, Bundle results) { |
| ContentValues values = new ContentValues(); |
| values.put("cmd", "stop"); |
| values.put("outputFileName", |
| Environment.getExternalStorageDirectory() + "/calendar-provider.ec"); |
| mContentResolver.insert(EMMA_CONTENT_URI, values); |
| super.finish(resultCode, results); |
| } |
| } |
| } |