blob: c93191774e26330ce409877c161413e940eb9c88 [file] [log] [blame]
/*
* 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 dalvik.annotation.TestTargetClass;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Instances;
import android.provider.CalendarContract.Reminders;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.*;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
@TestTargetClass(android.provider.CalendarContract.class)
public class CalendarTest extends InstrumentationTestCase {
private static final String TAG = "CalCTS";
private static final String CTS_TEST_TYPE = "LOCAL";
// @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_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.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
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, seed);
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);
}
}
// @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.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
};
// @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 % 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]);
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_ALARM, 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.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_ALARM, 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);
}
}
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();
}
@Override
protected void setUp() throws Exception {
super.setUp();
mContentResolver = getInstrumentation().getTargetContext().getContentResolver();
// IContentProvider provider = mContentResolver.acquireProvider(Calendar.AUTHORITY);
// mBuilder = new ContactsContract_TestDataBuilder(provider);
}
@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);
}
@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);
// 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);
removeAndVerifyCalendar(account, id);
}
// TODO test calendar updates as sync adapter
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);
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);
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();
}
/**
* 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");
if (false) {
Cursor instances = getInstances(timeZone, "2003-08-05T00:00:00", "2003-08-31T11:59:59",
new String[] { Instances.BEGIN });
dumpInstances(instances, timeZone, "initial");
}
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.
String timeZone = eventValues.getAsString(Events.EVENT_TIMEZONE);
String testStart = "1999-03-28T00:00:00";
String testEnd = "1999-04-25T23:59:59";
String[] projection = { Instances.BEGIN, Instances.START_MINUTE, Instances.END_MINUTE };
Cursor instances = getInstances(timeZone, testStart, testEnd, projection);
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);
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);
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);
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);
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);
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);
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 creation of a simple recurrence exception where the exception is created first.
* Simulates out-of-order delivery from the server.
* <p>
* We can't use the /exception URI, because that only works if the events are created
* in order.
*/
@MediumTest
public void testOutOfOrderRecurrenceExceptions() {
String account = "reooo_account";
String startWhen = "1987-08-09T12:00:00";
int seed = 0;
// Clean up just in case
CalendarHelper.deleteCalendarByAccount(mContentResolver, account);
// Create calendar
long calendarId = createAndVerifyCalendar(account, seed++, null);
// Generate base event.
ContentValues eventValues = EventHelper.getNewRecurringEventValues(account, seed,
calendarId, true, startWhen, "PT1H", "FREQ=DAILY;WKST=SU;COUNT=10");
// Select a period that gives us 3 instances.
String timeZone = eventValues.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(account, calendarId, testStart, testEnd, timeZone);
/*
* Instances table should be expanded. Do the test.
*/
// Generate exception from base. We shift the start time by half an hour.
ContentValues excepValues = new ContentValues(eventValues);
excepValues.remove(Events._SYNC_ID);
excepValues.put(Events.ORIGINAL_SYNC_ID, eventValues.getAsString(Events._SYNC_ID));
long dtstartMillis = excepValues.getAsLong(Events.DTSTART);
dtstartMillis += 24 * 60 * 60 * 1000; // add one day -- use the second instance
excepValues.put(Events.ORIGINAL_INSTANCE_TIME, dtstartMillis);
dtstartMillis += 1800 * 1000;
excepValues.put(Events.DTSTART, dtstartMillis);
excepValues.put(Events.DTEND, dtstartMillis + 3600*1000);
excepValues.remove(Events.DURATION);
excepValues.remove(Events.RRULE);
// Create exception event.
long excepId = createAndVerifyEvent(account, seed, calendarId, true, excepValues);
assertTrue(excepId >= 0);
// Create recurring event.
long eventId = createAndVerifyEvent(account, seed, calendarId, true, eventValues);
assertTrue(eventId >= 0);
// Check to see how many instances we get. If the recurrence and the exception don't
// get paired up correctly, we'll see one instance too many.
Cursor instances = getInstances(timeZone, testStart, testEnd, projection);
if (DEBUG_RECURRENCE) {
dumpInstances(instances, timeZone, "with exception");
}
assertEquals("initial recurrence instance count", 3, instances.getCount());
instances.close();
// delete the calendar
removeAndVerifyCalendar(account, calendarId);
}
/**
* 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);
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);
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);
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);
assertEquals("post-update instance count", 3, instances.getCount());
while (instances.moveToNext()) {
assertEquals("new location", newLocation, instances.getString(1));
}
// 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);
}
/**
* 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) {
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.
Uri uri = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI,
startMillis + "/" + endMillis);
Cursor instances = mContentResolver.query(uri, projection, null, 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) {
Cursor instances = getInstances(timeZone, startWhen, endWhen,
new String[] { Instances._ID });
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);
// 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 attendees to an event.
*/
private void addAttendees(String account, long eventId, int seed) {
assertTrue(eventId >= 0);
Uri syncUri = asSyncAdapter(Attendees.CONTENT_URI, account, CTS_TEST_TYPE);
ContentValues values = new ContentValues();
values.put(Attendees.EVENT_ID, eventId);
values.put(Attendees.ATTENDEE_NAME, "Attender" + seed);
values.put(Attendees.ATTENDEE_EMAIL, CalendarHelper.generateCalendarOwnerEmail(account));
values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
Uri uri = mContentResolver.insert(syncUri, values);
seed++;
values = new ContentValues();
values.put(Attendees.EVENT_ID, eventId);
values.put(Attendees.ATTENDEE_NAME, "Attender" + seed);
values.put(Attendees.ATTENDEE_EMAIL, "attender" + seed + "@example.com");
values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_TENTATIVE);
values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_NONE);
values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
uri = mContentResolver.insert(syncUri, values);
}
/**
* Add some reminders to an event.
*/
private void addReminders(String account, long eventId, int seed) {
ContentValues values = new ContentValues();
values.put(Reminders.EVENT_ID, eventId);
values.put(Reminders.MINUTES, seed * 5);
values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
Uri syncUri = asSyncAdapter(Reminders.CONTENT_URI, account, CTS_TEST_TYPE);
Uri uri = mContentResolver.insert(syncUri, values);
}
/**
* 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);
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);
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);
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. This assumes
* a single calendar has been created on the given account.
*
* @param account the account of the calendar
* @param values the values to check against the db
* @param id the _id of the calendar
*/
private void verifyCalendar(String account, ContentValues values, long id) {
// Verify
Cursor c = CalendarHelper.getCalendarsByAccount(mContentResolver, account);
assertEquals(1, c.getCount());
assertTrue(c.moveToFirst());
assertEquals(id, c.getLong(0));
for (String key : values.keySet()) {
int index = c.getColumnIndex(key);
assertEquals(key, values.getAsString(key), c.getString(index));
}
c.close();
}
}