blob: 9e9e6e55c9b7cda6f55e6173684b2013941886af [file] [log] [blame]
/*
* Copyright (C) 2008 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 com.android.providers.calendar;
import com.android.internal.database.ArrayListCursor;
import android.content.*;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.provider.Calendar;
import android.provider.Calendar.BusyBits;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
import android.provider.Calendar.Instances;
import android.test.ProviderTestCase2;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.Suppress;
import android.util.Log;
import java.util.ArrayList;
/**
* Runs various tests on an isolated Calendar provider with its own database.
*/
public class CalendarProviderTest extends ProviderTestCase2<CalendarProvider> {
static final String TAG = "calendar";
static final String DEFAULT_TIMEZONE = "America/Los_Angeles";
private SQLiteDatabase mDb;
private MetaData mMetaData;
private Context mContext;
private MockContentResolver mResolver;
private Uri mEventsUri = Uri.parse("content://calendar/events");
private int mCalendarId;
protected boolean mWipe = false;
// We need a unique id to put in the _sync_id field so that we can create
// recurrence exceptions that refer to recurring events.
private int mGlobalSyncId = 1000;
/**
* KeyValue is a simple class that stores a pair of strings representing
* a (key, value) pair. This is used for updating events.
*/
private class KeyValue {
String key;
String value;
public KeyValue(String key, String value) {
this.key = key;
this.value = value;
}
}
/**
* A generic command interface. This is used to support a sequence of
* commands that can create events, delete or update events, and then
* check that the state of the database is as expected.
*/
private interface Command {
public void execute();
}
/**
* This is used to insert a new event into the database. The event is
* specified by its name (or "title"). All of the event fields (the
* start and end time, whether it is an all-day event, and so on) are
* stored in a separate table (the "mEvents" table).
*/
private class Insert implements Command {
EventInfo eventInfo;
public Insert(String eventName) {
eventInfo = findEvent(eventName);
}
public void execute() {
Log.i(TAG, "insert " + eventInfo.mTitle);
insertEvent(mCalendarId, eventInfo);
}
}
/**
* This is used to delete an event, specified by the event name.
*/
private class Delete implements Command {
String eventName;
int expected;
public Delete(String eventName, int expected) {
this.eventName = eventName;
this.expected = expected;
}
public void execute() {
Log.i(TAG, "delete " + eventName);
int rows = deleteMatchingEvents(eventName);
assertEquals(expected, rows);
}
}
/**
* This is used to update an event. The values to update are specified
* with an array of (key, value) pairs. Both the key and value are
* specified as strings. Event fields that are not really strings (such
* as DTSTART which is a long) should be converted to the appropriate type
* but that isn't supported yet. When needed, that can be added here
* by checking for specific keys and converting the associated values.
*/
private class Update implements Command {
String eventName;
KeyValue[] pairs;
public Update(String eventName, KeyValue[] pairs) {
this.eventName = eventName;
this.pairs = pairs;
}
public void execute() {
Log.i(TAG, "update " + eventName);
if (mWipe) {
// Wipe instance table so it will be regenerated
mMetaData.clearInstanceRange();
}
ContentValues map = new ContentValues();
for (KeyValue pair : pairs) {
String value = pair.value;
if (Calendar.EventsColumns.STATUS.equals(pair.key)) {
// Do type conversion for STATUS
map.put(pair.key, Integer.parseInt(value));
} else {
map.put(pair.key, value);
}
}
updateMatchingEvents(eventName, map);
}
}
/**
* This command queries the number of events and compares it to the given
* expected value.
*/
private class QueryNumEvents implements Command {
int expected;
public QueryNumEvents(int expected) {
this.expected = expected;
}
public void execute() {
Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(expected, cursor.getCount());
cursor.close();
}
}
/**
* This command dumps the list of events to the log for debugging.
*/
private class DumpEvents implements Command {
public DumpEvents() {
}
public void execute() {
Cursor cursor = mResolver.query(mEventsUri, null, null, null, null);
dumpCursor(cursor);
cursor.close();
}
}
/**
* This command dumps the list of instances to the log for debugging.
*/
private class DumpInstances implements Command {
long begin;
long end;
public DumpInstances(String startDate, String endDate) {
Time time = new Time(DEFAULT_TIMEZONE);
time.parse3339(startDate);
begin = time.toMillis(false /* use isDst */);
time.parse3339(endDate);
end = time.toMillis(false /* use isDst */);
}
public void execute() {
Cursor cursor = queryInstances(begin, end);
dumpCursor(cursor);
cursor.close();
}
}
/**
* This command queries the number of instances and compares it to the given
* expected value.
*/
private class QueryNumInstances implements Command {
int expected;
long begin;
long end;
public QueryNumInstances(String startDate, String endDate, int expected) {
Time time = new Time(DEFAULT_TIMEZONE);
time.parse3339(startDate);
begin = time.toMillis(false /* use isDst */);
time.parse3339(endDate);
end = time.toMillis(false /* use isDst */);
this.expected = expected;
}
public void execute() {
Cursor cursor = queryInstances(begin, end);
assertEquals(expected, cursor.getCount());
cursor.close();
}
}
/**
* When this command runs it verifies that all of the instances in the
* given range match the expected instances (each instance is specified by
* a start date).
* If you just want to verify that an instance exists in a given date
* range, use {@link VerifyInstance} instead.
*/
private class VerifyAllInstances implements Command {
long[] instances;
long begin;
long end;
public VerifyAllInstances(String startDate, String endDate, String[] dates) {
Time time = new Time(DEFAULT_TIMEZONE);
time.parse3339(startDate);
begin = time.toMillis(false /* use isDst */);
time.parse3339(endDate);
end = time.toMillis(false /* use isDst */);
if (dates == null) {
return;
}
// Convert all the instance date strings to UTC milliseconds
int len = dates.length;
this.instances = new long[len];
int index = 0;
for (String instance : dates) {
time.parse3339(instance);
this.instances[index++] = time.toMillis(false /* use isDst */);
}
}
public void execute() {
Cursor cursor = queryInstances(begin, end);
int len = 0;
if (instances != null) {
len = instances.length;
}
if (len != cursor.getCount()) {
dumpCursor(cursor);
}
assertEquals("number of instances don't match", len, cursor.getCount());
if (instances == null) {
return;
}
int beginColumn = cursor.getColumnIndex(Instances.BEGIN);
while (cursor.moveToNext()) {
long begin = cursor.getLong(beginColumn);
// Search the list of expected instances for a matching start
// time.
boolean found = false;
for (long instance : instances) {
if (instance == begin) {
found = true;
break;
}
}
if (!found) {
int titleColumn = cursor.getColumnIndex(Events.TITLE);
int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY);
String title = cursor.getString(titleColumn);
boolean allDay = cursor.getInt(allDayColumn) != 0;
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE |
DateUtils.FORMAT_24HOUR;
if (allDay) {
flags |= DateUtils.FORMAT_UTC;
} else {
flags |= DateUtils.FORMAT_SHOW_TIME;
}
String date = DateUtils.formatDateRange(mContext, begin, begin, flags);
String mesg = String.format("Test failed!"
+ " unexpected instance (\"%s\") at %s",
title, date);
Log.e(TAG, mesg);
}
if (!found) {
dumpCursor(cursor);
}
assertTrue(found);
}
cursor.close();
}
}
/**
* When this command runs it verifies that the given instance exists in
* the given date range.
*/
private class VerifyInstance implements Command {
long instance;
boolean allDay;
long begin;
long end;
/**
* Creates a command to check that the given range [startDate,endDate]
* contains a specific instance of an event (specified by "date").
*
* @param startDate the beginning of the date range
* @param endDate the end of the date range
* @param date the date or date-time string of an event instance
*/
public VerifyInstance(String startDate, String endDate, String date) {
Time time = new Time(DEFAULT_TIMEZONE);
time.parse3339(startDate);
begin = time.toMillis(false /* use isDst */);
time.parse3339(endDate);
end = time.toMillis(false /* use isDst */);
// Convert the instance date string to UTC milliseconds
time.parse3339(date);
allDay = time.allDay;
instance = time.toMillis(false /* use isDst */);
}
public void execute() {
Cursor cursor = queryInstances(begin, end);
int beginColumn = cursor.getColumnIndex(Instances.BEGIN);
boolean found = false;
while (cursor.moveToNext()) {
long begin = cursor.getLong(beginColumn);
if (instance == begin) {
found = true;
break;
}
}
if (!found) {
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE;
if (allDay) {
flags |= DateUtils.FORMAT_UTC;
} else {
flags |= DateUtils.FORMAT_SHOW_TIME;
}
String date = DateUtils.formatDateRange(mContext, instance, instance, flags);
String mesg = String.format("Test failed!"
+ " cannot find instance at %s",
date);
Log.e(TAG, mesg);
}
assertTrue(found);
cursor.close();
}
}
/**
* This class stores all the useful information about an event.
*/
private class EventInfo {
String mTitle;
String mDescription;
String mTimezone;
boolean mAllDay;
long mDtstart;
long mDtend;
String mRrule;
String mDuration;
String mOriginalTitle;
long mOriginalInstance;
int mSyncId;
// Constructor for normal events, using the default timezone
public EventInfo(String title, String startDate, String endDate,
boolean allDay) {
init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE);
}
// Constructor for normal events, specifying the timezone
public EventInfo(String title, String startDate, String endDate,
boolean allDay, String timezone) {
init(title, startDate, endDate, allDay, timezone);
}
public void init(String title, String startDate, String endDate,
boolean allDay, String timezone) {
mTitle = title;
Time time = new Time();
if (allDay) {
time.timezone = Time.TIMEZONE_UTC;
} else if (timezone != null) {
time.timezone = timezone;
}
mTimezone = time.timezone;
time.parse3339(startDate);
mDtstart = time.toMillis(false /* use isDst */);
time.parse3339(endDate);
mDtend = time.toMillis(false /* use isDst */);
mDuration = null;
mRrule = null;
mAllDay = allDay;
}
// Constructor for repeating events, using the default timezone
public EventInfo(String title, String description, String startDate, String endDate,
String rrule, boolean allDay) {
init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE);
}
// Constructor for repeating events, specifying the timezone
public EventInfo(String title, String description, String startDate, String endDate,
String rrule, boolean allDay, String timezone) {
init(title, description, startDate, endDate, rrule, allDay, timezone);
}
public void init(String title, String description, String startDate, String endDate,
String rrule, boolean allDay, String timezone) {
mTitle = title;
mDescription = description;
Time time = new Time();
if (allDay) {
time.timezone = Time.TIMEZONE_UTC;
} else if (timezone != null) {
time.timezone = timezone;
}
mTimezone = time.timezone;
time.parse3339(startDate);
mDtstart = time.toMillis(false /* use isDst */);
if (endDate != null) {
time.parse3339(endDate);
mDtend = time.toMillis(false /* use isDst */);
}
if (allDay) {
long days = 1;
if (endDate != null) {
days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS;
}
mDuration = "P" + days + "D";
} else {
long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS;
mDuration = "P" + seconds + "S";
}
mRrule = rrule;
mAllDay = allDay;
}
// Constructor for recurrence exceptions, using the default timezone
public EventInfo(String originalTitle, String originalInstance, String title,
String description, String startDate, String endDate, boolean allDay) {
init(originalTitle, originalInstance,
title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE);
}
public void init(String originalTitle, String originalInstance,
String title, String description, String startDate, String endDate,
boolean allDay, String timezone) {
mOriginalTitle = originalTitle;
Time time = new Time(timezone);
time.parse3339(originalInstance);
mOriginalInstance = time.toMillis(false /* use isDst */);
init(title, description, startDate, endDate, null /* rrule */, allDay, timezone);
}
}
private class InstanceInfo {
EventInfo mEvent;
long mBegin;
long mEnd;
int mExpectedOccurrences;
public InstanceInfo(String eventName, String startDate, String endDate, int expected) {
// Find the test index that contains the given event name
mEvent = findEvent(eventName);
Time time = new Time(mEvent.mTimezone);
time.parse3339(startDate);
mBegin = time.toMillis(false /* use isDst */);
time.parse3339(endDate);
mEnd = time.toMillis(false /* use isDst */);
mExpectedOccurrences = expected;
}
}
private class BusyBitInfo {
EventInfo[] mEvents;
int mStartDay;
int mNumDays;
int[] mBusyBits;
int[] mAllDayCounts;
public BusyBitInfo(EventInfo[] events, String startDate, int numDays,
int[] busybits, int[] allDayCounts) {
mEvents = events;
Time time = new Time(DEFAULT_TIMEZONE);
time.parse3339(startDate);
long millis = time.toMillis(true /* ignore isDst */);
mStartDay = Time.getJulianDay(millis, time.gmtoff);
mNumDays = numDays;
mBusyBits = busybits;
mAllDayCounts = allDayCounts;
}
}
/**
* This is the main table of events. The events in this table are
* referred to by name in other places.
*/
private EventInfo[] mEvents = {
new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false),
new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false),
new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false),
new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true),
new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true),
new EventInfo("daily0", "daily from 5/1/2008 12am to 1am",
"2008-05-01T00:00:00", "2008-05-01T01:00:00",
"FREQ=DAILY;WKST=SU", false),
new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am",
"2008-05-01T08:30:00", "2008-05-01T09:30:00",
"FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false),
new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am",
"2008-05-01T08:45:00", "2008-05-01T09:15:00",
"FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false),
new EventInfo("allday daily0", "all-day daily from 5/1/2008",
"2008-05-01", null,
"FREQ=DAILY;WKST=SU", true),
new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008",
"2008-05-01", null,
"FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true),
new EventInfo("allday weekly0", "all-day weekly from 5/1/2008",
"2008-05-01", null,
"FREQ=WEEKLY;WKST=SU", true),
new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008",
"2008-05-01", "2008-05-03",
"FREQ=WEEKLY;WKST=SU", true),
new EventInfo("allday yearly0", "all-day yearly on 5/1/2008",
"2008-05-01T", null,
"FREQ=YEARLY;WKST=SU", true),
new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm",
"2008-05-06T13:00:00", "2008-05-06T14:00:00",
"FREQ=WEEKLY;BYDAY=TU;WKST=MO", false),
new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm",
"2008-05-06T14:30:00", "2008-05-06T15:30:00",
"FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false),
new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm",
"2008-05-20T15:00:00", "2008-05-20T16:00:00",
"FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false),
new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am",
"2008-05-01T00:00:00", "2008-05-01T00:10:00",
"FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false),
new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight",
"2008-05-31T23:00:00", "2008-06-01T00:00:00",
"FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false),
new EventInfo("daily0", "2008-05-01T00:00:00",
"except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am",
"2008-05-01T02:00:00", "2008-05-01T01:03:00", false),
new EventInfo("daily0", "2008-05-03T00:00:00",
"except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am",
"2008-05-03T02:00:00", "2008-05-03T01:03:00", false),
new EventInfo("daily0", "2008-05-02T00:00:00",
"except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008",
"2008-01-02T00:00:00", "2008-01-02T01:00:00", false),
new EventInfo("weekly0", "2008-05-13T13:00:00",
"except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm",
"2008-12-11T13:00:00", "2008-12-11T14:00:00", false),
new EventInfo("weekly0", "2008-05-13T13:00:00",
"cancel0", "weekly0 exception for 5/13/2008 1pm",
"2008-05-13T13:00:00", "2008-05-13T14:00:00", false),
new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm",
"2008-05-01T13:00:00", "2008-05-01T14:00:00",
"FREQ=YEARLY;WKST=SU", false),
};
/**
* This table is used to create repeating events and then check that the
* number of instances within a given range matches the expected number
* of instances.
*/
private InstanceInfo[] mInstanceRanges = {
new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1),
new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2),
new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2),
new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1),
new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2),
new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31),
new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32),
new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2),
new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1),
new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3),
new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7),
new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3),
new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1),
new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2),
new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5),
new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5),
new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1),
new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2),
new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4),
new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8),
new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0),
new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1),
new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2),
new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4),
new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0),
new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1),
new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0),
new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0),
new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1),
new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2),
new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1),
new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1),
new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1),
new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0),
new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2),
new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0),
new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1),
new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1),
new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2),
new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1),
new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2),
};
/**
* This tables of events is used to test the BusyBit database table.
*/
private EventInfo[] mBusyBitEvents = {
new EventInfo("1: 12am - 1am", "2008-05-01T00:00:00", "2008-05-01T01:00:00", false),
new EventInfo("2: 1:30am - 2am", "2008-05-02T01:30:00", "2008-05-02T02:00:00", false),
new EventInfo("3: 3am - 5am", "2008-05-03T03:00:00", "2008-05-03T05:00:00", false),
new EventInfo("4: 12am - 5am", "2008-05-04T00:00:00", "2008-05-04T05:00:00", false),
new EventInfo("5: 1am - 2am", "2008-05-05T01:00:00", "2008-05-05T02:00:00", false),
new EventInfo("5: 8am - 9am", "2008-05-05T08:00:00", "2008-05-05T09:00:00", false),
new EventInfo("6: 1am - 10am", "2008-05-06T01:00:00", "2008-05-06T10:00:00", false),
new EventInfo("6: 8am - 9am", "2008-05-06T08:00:00", "2008-05-06T09:00:00", false),
new EventInfo("7: 1am - 5am", "2008-05-07T01:00:00", "2008-05-07T05:00:00", false),
new EventInfo("7: 12am - 2am", "2008-05-07T00:00:00", "2008-05-07T02:00:00", false),
new EventInfo("7: 8am - 9am", "2008-05-07T08:00:00", "2008-05-07T09:00:00", false),
new EventInfo("7: 1pm - 2pm", "2008-05-07T13:00:00", "2008-05-07T14:00:00", false),
new EventInfo("7: 3:30pm - 4:30pm", "2008-05-07T15:30:00", "2008-05-07T16:30:00", false),
new EventInfo("7: 7pm - 8pm", "2008-05-07T19:00:00", "2008-05-07T20:00:00", false),
new EventInfo("7: 6:30pm - 7:30pm", "2008-05-07T18:30:00", "2008-05-07T19:30:00", false),
new EventInfo("7: 11pm - midnight", "2008-05-07T23:00:00", "2008-05-08T00:00:00", false),
new EventInfo("8: 1am - 2am", "2008-05-08T01:00:00", "2008-05-08T02:00:00", false),
new EventInfo("8: 3am - 4am", "2008-05-08T03:00:00", "2008-05-08T04:00:00", false),
new EventInfo("8: 5am - 6am", "2008-05-08T05:00:00", "2008-05-08T06:00:00", false),
new EventInfo("8: 7am - 8am", "2008-05-08T07:00:00", "2008-05-08T08:00:00", false),
new EventInfo("8: 9am - 10am", "2008-05-08T09:00:00", "2008-05-08T10:00:00", false),
new EventInfo("8: 11am - 12pm", "2008-05-08T11:00:00", "2008-05-08T12:00:00", false),
new EventInfo("8: 1pm - 2pm", "2008-05-08T13:00:00", "2008-05-08T14:00:00", false),
new EventInfo("8: 3pm - 4pm", "2008-05-08T15:00:00", "2008-05-08T16:00:00", false),
new EventInfo("8: 5pm - 6pm", "2008-05-08T17:00:00", "2008-05-08T18:00:00", false),
new EventInfo("8: 7pm - 8pm", "2008-05-08T19:00:00", "2008-05-08T20:00:00", false),
new EventInfo("8: 9pm - 10pm", "2008-05-08T21:00:00", "2008-05-08T22:00:00", false),
new EventInfo("8: 11pm - midnight", "2008-05-08T23:00:00", "2008-05-09T00:00:00", false),
new EventInfo("10: 12am - midnight", "2008-05-10T00:00:00", "2008-05-11T00:00:00", false),
new EventInfo("12: 1 day", "2008-05-12T00:00:00", "2008-05-13T00:00:00", true),
new EventInfo("14: 1 day", "2008-05-14T00:00:00", "2008-05-15T00:00:00", true),
new EventInfo("14: 2 days", "2008-05-14T00:00:00", "2008-05-16T00:00:00", true),
new EventInfo("14: 3 days", "2008-05-14T00:00:00", "2008-05-17T00:00:00", true),
new EventInfo("15: 1am - 2am", "2008-05-15T01:00:00", "2008-05-15T02:00:00", false),
new EventInfo("16: 10am - 11am", "2008-05-16T10:00:00", "2008-05-16T11:00:00", false),
new EventInfo("16: 11pm - midnight", "2008-05-16T23:00:00", "2008-05-17T00:00:00", false),
};
private EventInfo[] mBusyBitRepeatingEvents = {
new EventInfo("daily0", "daily from 5/1/2008 12am to 1am",
"2008-05-01T00:00:00", "2008-05-01T01:00:00",
"FREQ=DAILY;WKST=SU", false),
new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am",
"2008-05-01T08:30:00", "2008-05-01T09:30:00",
"FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false),
new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm",
"2008-05-06T13:00:00", "2008-05-06T14:00:00",
"FREQ=WEEKLY;BYDAY=TU;WKST=MO", false),
new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 4:30am to 5:30am",
"2008-05-06T04:30:00", "2008-05-06T05:30:00",
"FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false),
new EventInfo("weekly2", "weekly from 5/5/2008 on Mon 1 day",
"2008-05-05T00:00:00", "2008-05-06T00:00:00",
"FREQ=WEEKLY;BYDAY=MO;WKST=MO", true),
new EventInfo("weekly3", "weekly from 5/7/2008 on Wed 3 days",
"2008-05-07T00:00:00", "2008-05-10T00:00:00",
"FREQ=WEEKLY;BYDAY=WE;WKST=SU", true),
new EventInfo("weekly4", "weekly from 5/8/2008 on Thu 3 days",
"2008-05-08T00:00:00", "2008-05-11T00:00:00",
"FREQ=WEEKLY;BYDAY=TH;WKST=SU", true),
new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm",
"2008-05-20T15:00:00", "2008-05-20T16:00:00",
"FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false),
new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 11:00am to 11:10am",
"2008-05-01T11:00:00", "2008-05-01T11:10:00",
"FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false),
new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight",
"2008-05-31T23:00:00", "2008-06-01T00:00:00",
"FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false),
};
private BusyBitInfo[] mBusyBitTests = {
new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 1,
new int[] { 0x1 }, new int[] { 0 } ),
new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 1,
new int[] { 0x2 }, new int[] { 0 } ),
new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 2,
new int[] { 0x2, 0x18 }, new int[] { 0, 0 } ),
new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 3,
new int[] { 0x1, 0x2, 0x18 }, new int[] { 0, 0, 0 } ),
new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 8,
new int[] { 0x1, 0x2, 0x18, 0x1f, 0x102, 0x3fe, 0x8da11f, 0xaaaaaa },
new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } ),
new BusyBitInfo(mBusyBitEvents, "2008-05-10T00:00:00", 4,
new int[] { 0xffffff, 0x0, 0x0, 0x0 }, new int[] { 0, 0, 1, 0 } ),
new BusyBitInfo(mBusyBitEvents, "2008-05-14T00:00:00", 4,
new int[] { 0x0, 0x2, 0x800400, 0x0 }, new int[] { 3, 2, 1, 0 } ),
// Repeating events
new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 3,
new int[] { 0xb01, 0x301, 0x1 }, new int[] { 0, 0, 0 } ),
new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 10,
new int[] { 0xb01, 0x301, 0x1, 0x1, 0x1, 0x2031, 0x1, 0x1, 0x1, 0x1 },
new int[] { 0, 0, 0, 0, 1, 0, 1, 2, 2, 1 } ),
new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-18T00:00:00", 11,
new int[] { 0x1, 0x1, 0xa031, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2001, 0x1 },
new int[] { 0, 1, 0, 1, 2, 2, 1, 0, 1, 0, 1 } ),
new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-30T00:00:00", 5,
new int[] { 0x1, 0x800001, 0x801, 0x1, 0x2031 },
new int[] { 2, 1, 0, 1, 0 } ),
};
/**
* This sequence of commands inserts and deletes some events.
*/
private Command[] mNormalInsertDelete = {
new Insert("normal0"),
new Insert("normal1"),
new Insert("normal2"),
new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3),
new Delete("normal1", 1),
new QueryNumEvents(2),
new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2),
new Delete("normal1", 0),
new Delete("normal2", 1),
new QueryNumEvents(1),
new Delete("normal0", 1),
new QueryNumEvents(0),
};
/**
* This sequence of commands inserts and deletes some all-day events.
*/
private Command[] mAlldayInsertDelete = {
new Insert("allday0"),
new Insert("allday1"),
new QueryNumEvents(2),
new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0),
new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2),
new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
new Delete("allday0", 1),
new QueryNumEvents(1),
new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1),
new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1),
new Delete("allday1", 1),
new QueryNumEvents(0),
};
/**
* This sequence of commands inserts and deletes some repeating events.
*/
private Command[] mRecurringInsertDelete = {
new Insert("daily0"),
new Insert("daily1"),
new QueryNumEvents(2),
new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3),
new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2),
new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6),
new Delete("daily1", 1),
new QueryNumEvents(1),
new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2),
new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4),
new Delete("daily0", 1),
new QueryNumEvents(0),
};
/**
* This sequence of commands creates a recurring event with a recurrence
* exception that moves an event outside the expansion window. It checks that the
* recurrence exception does not occur in the Instances database table.
* Bug 1642665
*/
private Command[] mExceptionWithMovedRecurrence = {
new Insert("daily0"),
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
"2008-05-03T00:00:00", }),
new Insert("except2"),
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}),
};
/**
* This sequence of commands deletes (cancels) one instance of a recurrence.
*/
private Command[] mCancelInstance = {
new Insert("weekly0"),
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00",
"2008-05-20T13:00:00", }),
new Insert("cancel0"),
new Update("cancel0", new KeyValue[] {
new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED),
}),
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
new String[] {"2008-05-06T13:00:00",
"2008-05-20T13:00:00", }),
};
/**
* This sequence of commands creates a recurring event with a recurrence
* exception that moves an event from outside the expansion window into the
* expansion window.
*/
private Command[] mExceptionWithMovedRecurrence2 = {
new Insert("weekly0"),
new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
"2008-12-16T13:00:00", }),
new Insert("except3"),
new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00",
new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00",
"2008-12-11T13:00:00", "2008-12-16T13:00:00", }),
};
/**
* This sequence of commands creates a recurring event with a recurrence
* exception and then changes the end time of the recurring event. It then
* checks that the recurrence exception does not occur in the Instances
* database table.
*/
private Command[]
mExceptionWithTruncatedRecurrence = {
new Insert("daily0"),
// Verify 4 occurrences of the "daily0" repeating event
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
"2008-05-03T00:00:00", "2008-05-04T00:00:00"}),
new Insert("except1"),
new QueryNumEvents(2),
// Verify that one of the 4 occurrences has its start time changed
// so that it now matches the recurrence exception.
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
"2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
// Change the end time of "daily0" but it still includes the
// recurrence exception.
new Update("daily0", new KeyValue[] {
new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"),
}),
// Verify that the recurrence exception is still there
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00",
"2008-05-03T02:00:00", "2008-05-04T00:00:00"}),
// This time change the end time of "daily0" so that it excludes
// the recurrence exception.
new Update("daily0", new KeyValue[] {
new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"),
}),
// The server will cancel the out-of-range exception.
// It would be nice for the provider to handle this automatically,
// but for now simulate the server-side cancel.
new Update("except1", new KeyValue[] {
new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED),
}),
// Verify that the recurrence exception does not appear.
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00",
new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}),
};
/**
* Bug 135848. Ensure that a recurrence exception is displayed even if the recurrence
* is not present.
*/
private Command[] mExceptionWithNoRecurrence = {
new Insert("except0"),
new QueryNumEvents(1),
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00",
new String[] {"2008-05-01T02:00:00"}),
};
private EventInfo findEvent(String name) {
int len = mEvents.length;
for (int ii = 0; ii < len; ii++) {
EventInfo event = mEvents[ii];
if (name.equals(event.mTitle)) {
return event;
}
}
return null;
}
public CalendarProviderTest() {
super(CalendarProvider.class, Calendar.AUTHORITY);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mContext = getMockContext();
mResolver = getMockContentResolver();
mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
mResolver.addProvider("sync", new MockProvider("sync"));
mDb = getProvider().getDatabase();
mMetaData = getProvider().mMetaData;
}
@Override
protected void tearDown() throws Exception {
mDb.close();
mDb = null;
super.tearDown();
}
/**
* Dumps the contents of the given cursor to the log. For debugging.
* @param cursor the database cursor
*/
private void dumpCursor(Cursor cursor) {
cursor.moveToPosition(-1);
String[] cols = cursor.getColumnNames();
Log.i(TAG, "dumpCursor() count: " + cursor.getCount());
int index = 0;
while (cursor.moveToNext()) {
Log.i(TAG, index + " {");
for (int i = 0; i < cols.length; i++) {
Log.i(TAG, " " + cols[i] + '=' + cursor.getString(i));
}
Log.i(TAG, "}");
index += 1;
}
cursor.moveToPosition(-1);
}
private int insertCal(String name, String timezone) {
ContentValues m = new ContentValues();
m.put(Calendars.NAME, name);
m.put(Calendars.DISPLAY_NAME, name);
m.put(Calendars.COLOR, "0xff123456");
m.put(Calendars.TIMEZONE, timezone);
m.put(Calendars.SELECTED, 1);
m.put(Calendars.URL, "http://www.google.com/calendar/feeds/joe%40joe.com/private/full");
m.put(Calendars.OWNER_ACCOUNT, "joe@joe.com");
Uri url = mResolver.insert(Uri.parse("content://calendar/calendars"), m);
String id = url.getLastPathSegment();
return Integer.parseInt(id);
}
private Uri insertEvent(int calId, EventInfo event) {
if (mWipe) {
// Wipe instance table so it will be regenerated
mMetaData.clearInstanceRange();
}
ContentValues m = new ContentValues();
m.put(Events.CALENDAR_ID, calId);
m.put(Events.TITLE, event.mTitle);
m.put(Events.DTSTART, event.mDtstart);
m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0);
if (event.mRrule == null) {
// This is a normal event
m.put(Events.DTEND, event.mDtend);
} else {
// This is a repeating event
m.put(Events.RRULE, event.mRrule);
m.put(Events.DURATION, event.mDuration);
}
if (event.mDescription != null) {
m.put(Events.DESCRIPTION, event.mDescription);
}
if (event.mTimezone != null) {
m.put(Events.EVENT_TIMEZONE, event.mTimezone);
}
if (event.mOriginalTitle != null) {
// This is a recurrence exception.
EventInfo recur = findEvent(event.mOriginalTitle);
assertNotNull(recur);
String syncId = String.format("%d", recur.mSyncId);
m.put(Events.ORIGINAL_EVENT, syncId);
m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0);
m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
}
Uri url = mResolver.insert(mEventsUri, m);
// Create a fake _sync_id and add it to the event. Update the database
// directly so that we don't trigger any validation checks in the
// CalendarProvider.
long id = ContentUris.parseId(url);
mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id);
event.mSyncId = mGlobalSyncId;
mGlobalSyncId += 1;
return url;
}
/**
* Deletes all the events that match the given title.
* @param title the given title to match events on
* @return the number of rows deleted
*/
private int deleteMatchingEvents(String title) {
Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID },
"title=?", new String[] { title }, null);
int numRows = 0;
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
numRows += mResolver.delete(uri, null, null);
}
cursor.close();
return numRows;
}
/**
* Updates all the events that match the given title.
* @param title the given title to match events on
* @return the number of rows updated
*/
private int updateMatchingEvents(String title, ContentValues values) {
String[] projection = new String[] {
Events._ID,
Events.DTSTART,
Events.DTEND,
Events.DURATION,
Events.ALL_DAY,
Events.RRULE,
Events.EVENT_TIMEZONE,
Events.ORIGINAL_EVENT,
};
Cursor cursor = mResolver.query(mEventsUri, projection,
"title=?", new String[] { title }, null);
int numRows = 0;
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
// If any of the following fields are being changed, then we need
// to include all of them.
if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND)
|| values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY)
|| values.containsKey(Events.RRULE)
|| values.containsKey(Events.EVENT_TIMEZONE)
|| values.containsKey(Calendar.EventsColumns.STATUS)) {
long dtstart = cursor.getLong(1);
long dtend = cursor.getLong(2);
String duration = cursor.getString(3);
boolean allDay = cursor.getInt(4) != 0;
String rrule = cursor.getString(5);
String timezone = cursor.getString(6);
String originalEvent = cursor.getString(7);
if (!values.containsKey(Events.DTSTART)) {
values.put(Events.DTSTART, dtstart);
}
// Don't add DTEND for repeating events
if (!values.containsKey(Events.DTEND) && rrule == null) {
values.put(Events.DTEND, dtend);
}
if (!values.containsKey(Events.DURATION) && duration != null) {
values.put(Events.DURATION, duration);
}
if (!values.containsKey(Events.ALL_DAY)) {
values.put(Events.ALL_DAY, allDay ? 1 : 0);
}
if (!values.containsKey(Events.RRULE) && rrule != null) {
values.put(Events.RRULE, rrule);
}
if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) {
values.put(Events.EVENT_TIMEZONE, timezone);
}
if (!values.containsKey(Events.ORIGINAL_EVENT) && originalEvent != null) {
values.put(Events.ORIGINAL_EVENT, originalEvent);
}
}
Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
numRows += mResolver.update(uri, values, null, null);
}
cursor.close();
return numRows;
}
private void deleteAllEvents() {
mDb.execSQL("DELETE FROM Events;");
mMetaData.clearInstanceRange();
}
@LargeTest
public void testInsertNormalEvents() throws Exception {
Cursor cursor;
Uri url = null;
int calId = insertCal("Calendar0", DEFAULT_TIMEZONE);
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
// Keep track of the number of normal events
int numEvents = 0;
// "begin" is the earliest start time of all the normal events,
// and "end" is the latest end time of all the normal events.
long begin = 0, end = 0;
int len = mEvents.length;
for (int ii = 0; ii < len; ii++) {
EventInfo event = mEvents[ii];
// Skip repeating events and recurrence exceptions
if (event.mRrule != null || event.mOriginalTitle != null) {
continue;
}
if (numEvents == 0) {
begin = event.mDtstart;
end = event.mDtend;
} else {
if (begin > event.mDtstart) {
begin = event.mDtstart;
}
if (end < event.mDtend) {
end = event.mDtend;
}
}
url = insertEvent(calId, event);
numEvents += 1;
}
// query one
cursor = mResolver.query(url, null, null, null, null);
assertEquals(1, cursor.getCount());
cursor.close();
// query all
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(numEvents, cursor.getCount());
cursor.close();
// Check that the Instances table has one instance of each of the
// normal events.
cursor = queryInstances(begin, end);
assertEquals(numEvents, cursor.getCount());
cursor.close();
}
@LargeTest
public void testInsertRepeatingEvents() throws Exception {
Cursor cursor;
Uri url = null;
int calId = insertCal("Calendar0", "America/Los_Angeles");
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
// Keep track of the number of repeating events
int numEvents = 0;
int len = mEvents.length;
for (int ii = 0; ii < len; ii++) {
EventInfo event = mEvents[ii];
// Skip normal events
if (event.mRrule == null) {
continue;
}
url = insertEvent(calId, event);
numEvents += 1;
}
// query one
cursor = mResolver.query(url, null, null, null, null);
assertEquals(1, cursor.getCount());
cursor.close();
// query all
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(numEvents, cursor.getCount());
cursor.close();
}
// TODO: flaky test, temporarily remove from continuous.
// @LargeTest
public void testInstanceRange() throws Exception {
Cursor cursor;
Uri url = null;
int calId = insertCal("Calendar0", "America/Los_Angeles");
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
int len = mInstanceRanges.length;
for (int ii = 0; ii < len; ii++) {
InstanceInfo instance = mInstanceRanges[ii];
EventInfo event = instance.mEvent;
url = insertEvent(calId, event);
cursor = queryInstances(instance.mBegin, instance.mEnd);
if (instance.mExpectedOccurrences != cursor.getCount()) {
Log.e(TAG, "Test failed! Instance index: " + ii);
Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription
+ " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]"
+ " expected: " + instance.mExpectedOccurrences);
dumpCursor(cursor);
}
assertEquals(instance.mExpectedOccurrences, cursor.getCount());
cursor.close();
int rows = mResolver.delete(url, null /* selection */, null /* selection args */);
assertEquals(1, rows);
}
}
@LargeTest
public void testBusyBitRange() throws Exception {
Cursor cursor;
Uri url = null;
int calId = insertCal("Calendar0", "America/Los_Angeles");
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
int len = mBusyBitTests.length;
for (int ii = 0; ii < len; ii++) {
deleteAllEvents();
BusyBitInfo busyInfo = mBusyBitTests[ii];
EventInfo[] events = busyInfo.mEvents;
int numEvents = events.length;
for (int jj = 0; jj < numEvents; jj++) {
EventInfo event = events[jj];
insertEvent(calId, event);
}
int startDay = busyInfo.mStartDay;
int numDays = busyInfo.mNumDays;
int[] busybits = new int[numDays];
int[] allDayCounts = new int[numDays];
if (false) {
cursor = mResolver.query(mEventsUri, null, null, null, null);
Log.i(TAG, "Dump of Events table, count: " + cursor.getCount());
dumpCursor(cursor);
cursor.close();
Time time = new Time();
time.setJulianDay(startDay);
long begin = time.toMillis(true);
int endDay = startDay + numDays - 1;
time.setJulianDay(endDay);
long end = time.toMillis(true);
cursor = queryInstances(begin, end);
Log.i(TAG, "Dump of Instances table, count: " + cursor.getCount()
+ " startDay: " + startDay + " endDay: " + endDay
+ " begin: " + begin + " end: " + end);
dumpCursor(cursor);
cursor.close();
}
cursor = queryBusyBits(startDay, numDays);
try {
int dayColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.DAY);
int busybitColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.BUSYBITS);
int allDayCountColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.ALL_DAY_COUNT);
while (cursor.moveToNext()) {
int day = cursor.getInt(dayColumnIndex);
int dayIndex = day - startDay;
busybits[dayIndex] = cursor.getInt(busybitColumnIndex);
allDayCounts[dayIndex] = cursor.getInt(allDayCountColumnIndex);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
// Compare the database busy bits with the expected busy bits
for (int dayIndex = 0; dayIndex < numDays; dayIndex++) {
if (busyInfo.mBusyBits[dayIndex] != busybits[dayIndex]) {
String mesg = String.format("Test failed!"
+ " BusyBit test index: %d"
+ " day index: %d"
+ " mStartDay: %d mNumDays: %d"
+ " expected busybits: 0x%x was: 0x%x",
ii, dayIndex, busyInfo.mStartDay, busyInfo.mNumDays,
busyInfo.mBusyBits[dayIndex], busybits[dayIndex]);
Log.e(TAG, mesg);
cursor = mResolver.query(mEventsUri, null, null, null, null);
Log.i(TAG, "Dump of Events table, count: " + cursor.getCount());
dumpCursor(cursor);
cursor.close();
}
assertEquals(busyInfo.mBusyBits[dayIndex], busybits[dayIndex]);
}
// Compare the database all-day counts with the expected all-day counts
for (int dayIndex = 0; dayIndex < numDays; dayIndex++) {
if (busyInfo.mAllDayCounts[dayIndex] != allDayCounts[dayIndex]) {
String mesg = String.format("Test failed!"
+ " BusyBit test index: %d"
+ " day index: %d"
+ " expected all-day count: %d was: %d",
ii, dayIndex,
busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]);
Log.e(TAG, mesg);
}
assertEquals(busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]);
}
}
}
@LargeTest
public void testEntityQuery() throws Exception {
testInsertNormalEvents(); // To initialize
ContentValues reminder = new ContentValues();
reminder.put(Calendar.Reminders.EVENT_ID, 1);
reminder.put(Calendar.Reminders.MINUTES, 10);
reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_SMS);
mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
reminder.put(Calendar.Reminders.MINUTES, 20);
mResolver.insert(Calendar.Reminders.CONTENT_URI, reminder);
ContentValues extended = new ContentValues();
extended.put(Calendar.ExtendedProperties.NAME, "foo");
extended.put(Calendar.ExtendedProperties.VALUE, "bar");
extended.put(Calendar.ExtendedProperties.EVENT_ID, 2);
mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
extended.put(Calendar.ExtendedProperties.EVENT_ID, 1);
mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
extended.put(Calendar.ExtendedProperties.NAME, "foo2");
extended.put(Calendar.ExtendedProperties.VALUE, "bar2");
mResolver.insert(Calendar.ExtendedProperties.CONTENT_URI, extended);
ContentValues attendee = new ContentValues();
attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
Calendar.Attendees.RELATIONSHIP_PERFORMER);
attendee.put(Calendar.Attendees.EVENT_ID, 3);
mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
EntityIterator ei = mResolver.queryEntities(mEventsUri, null, null, null);
int count = 0;
while (ei.hasNext()) {
Entity entity = ei.next();
ContentValues values = entity.getEntityValues();
ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
switch (values.getAsInteger("_id")) {
case 1:
assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties
break;
case 2:
assertEquals(1, subvalues.size()); // Extended properties
break;
case 3:
assertEquals(1, subvalues.size()); // Attendees
break;
default:
assertEquals(0, subvalues.size());
break;
}
count += 1;
}
assertEquals(5, count);
ei = mResolver.queryEntities(mEventsUri, "Events._id = 3", null, null);
count = 0;
while (ei.hasNext()) {
Entity entity = ei.next();
count += 1;
}
assertEquals(1, count);
}
/**
* Test attendee processing
* @throws Exception
*/
@LargeTest
public void testAttendees() throws Exception {
mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
long eventId = ContentUris.parseId(eventUri);
ContentValues attendee = new ContentValues();
attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
Calendar.Attendees.RELATIONSHIP_ORGANIZER);
attendee.put(Calendar.Attendees.EVENT_ID, eventId);
Uri attendeesUri = mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
Cursor cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
"event_id=" + eventId, null, null);
assertEquals(1, cursor.getCount());
cursor.close();
cursor = mResolver.query(eventUri, null, null, null, null);
int selfColumn = cursor.getColumnIndex(Calendar.Events.SELF_ATTENDEE_STATUS);
cursor.moveToNext();
long selfAttendeeStatus = cursor.getInt(selfColumn);
assertEquals(Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus);
cursor.close();
// Change status to declined
attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
Calendar.Attendees.ATTENDEE_STATUS_DECLINED);
mResolver.update(attendeesUri, attendee, null, null);
cursor = mResolver.query(eventUri, null, null, null, null);
cursor.moveToNext();
selfAttendeeStatus = cursor.getInt(selfColumn);
assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
cursor.close();
// Add another attendee
attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Dude");
attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "dude@dude.com");
attendee.put(Calendar.Attendees.ATTENDEE_STATUS,
Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED);
mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
cursor = mResolver.query(Calendar.Attendees.CONTENT_URI, null,
"event_id=" + mCalendarId, null, null);
assertEquals(2, cursor.getCount());
cursor.close();
cursor = mResolver.query(eventUri, null, null, null, null);
cursor.moveToNext();
selfAttendeeStatus = cursor.getInt(selfColumn);
assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
cursor.close();
// This sleep is a big hack. The problem is that CalendarProvider has a TimezoneChecker
// thread running in the background, and if the test finishes before TimezoneChecker
// finishes, the thread will try to access the closed database and mess it up.
// It appears that testAttendees completes fast enough to trigger this, but the other
// test cases don't.
Thread.sleep(1000);
}
/**
* Run commands, wiping instance table at each step.
* This tests full instance expansion.
* @throws Exception
*/
@LargeTest
public void testCommandSequences1() throws Exception {
commandSequences(true);
}
/**
* Run commands normally.
* This tests incremental instance expansion.
* @throws Exception
*/
@LargeTest
public void testCommandSequences2() throws Exception {
commandSequences(false);
}
/**
* Run thorough set of command sequences
* @param wipe true if instances should be wiped and regenerated
* @throws Exception
*/
private void commandSequences(boolean wipe) throws Exception {
Cursor cursor;
Uri url = null;
mWipe = wipe; // Set global flag
mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
Command[] commands;
Log.i(TAG, "Normal insert/delete");
commands = mNormalInsertDelete;
for (Command command : commands) {
command.execute();
}
deleteAllEvents();
Log.i(TAG, "All-day insert/delete");
commands = mAlldayInsertDelete;
for (Command command : commands) {
command.execute();
}
deleteAllEvents();
Log.i(TAG, "Recurring insert/delete");
commands = mRecurringInsertDelete;
for (Command command : commands) {
command.execute();
}
deleteAllEvents();
Log.i(TAG, "Exception with truncated recurrence");
commands = mExceptionWithTruncatedRecurrence;
for (Command command : commands) {
command.execute();
}
deleteAllEvents();
Log.i(TAG, "Exception with moved recurrence");
commands = mExceptionWithMovedRecurrence;
for (Command command : commands) {
command.execute();
}
deleteAllEvents();
Log.i(TAG, "Exception with cancel");
commands = mCancelInstance;
for (Command command : commands) {
command.execute();
}
deleteAllEvents();
Log.i(TAG, "Exception with moved recurrence2");
commands = mExceptionWithMovedRecurrence2;
for (Command command : commands) {
command.execute();
}
deleteAllEvents();
Log.i(TAG, "Exception with no recurrence");
commands = mExceptionWithNoRecurrence;
for (Command command : commands) {
command.execute();
}
}
/**
* Test Time toString.
* @throws Exception
*/
// Suppressed because toString currently hangs.
@Suppress
public void testTimeToString() throws Exception {
Time time = new Time(Time.TIMEZONE_UTC);
String str = "2039-01-01T23:00:00.000Z";
String result = "20390101T230000UTC(0,0,0,-1,0)";
time.parse3339(str);
assertEquals(result, time.toString());
}
private Cursor queryInstances(long begin, long end) {
Uri url = Uri.parse("content://calendar/instances/when/" + begin + "/" + end);
return mResolver.query(url, null, null, null, null);
}
private Cursor queryBusyBits(int startDay, int numDays) {
int endDay = startDay + numDays - 1;
Uri url = Uri.parse("content://calendar/busybits/when/" + startDay + "/" + endDay);
return mResolver.query(url, null, null, null, null);
}
protected static class MockProvider extends ContentProvider {
private String mAuthority;
private int mNumItems = 0;
public MockProvider(String authority) {
mAuthority = authority;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>());
}
@Override
public String getType(Uri uri) {
throw new UnsupportedOperationException();
}
@Override
public Uri insert(Uri uri, ContentValues values) {
mNumItems++;
return Uri.parse("content://" + mAuthority + "/" + mNumItems);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
return 0;
}
}
}