blob: 2e2f5530dfe758f260cf096000cd5fcbaabfe4dd [file] [log] [blame]
/*
* Copyright (C) 2010 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.exchange.adapter;
import com.android.exchange.adapter.AbstractSyncAdapter.Operation;
import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations;
import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
import com.android.exchange.provider.MockProvider;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.res.Resources;
import android.database.Cursor;
import android.os.RemoteException;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Events;
import android.test.IsolatedContext;
import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;
import android.test.mock.MockContext;
import android.test.suitebuilder.annotation.MediumTest;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
/**
* You can run this entire test case with:
* runtest -c com.android.exchange.adapter.CalendarSyncAdapterTests exchange
*/
@MediumTest
public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
private static final String[] ATTENDEE_PROJECTION = new String[] {Attendees.ATTENDEE_EMAIL,
Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_STATUS};
private static final int ATTENDEE_EMAIL = 0;
private static final int ATTENDEE_NAME = 1;
private static final int ATTENDEE_STATUS = 2;
private static final String SINGLE_ATTENDEE_EMAIL = "attendee@host.com";
private static final String SINGLE_ATTENDEE_NAME = "Bill Attendee";
private Context mMockContext;
private MockContentResolver mMockResolver;
// This is the US/Pacific time zone as a base64-encoded TIME_ZONE_INFORMATION structure, as
// it would appear coming from an Exchange server
private static final String TEST_TIME_ZONE = "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
"GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
"QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
private class MockContext2 extends MockContext {
@Override
public Resources getResources() {
return getContext().getResources();
}
@Override
public File getDir(String name, int mode) {
// name the directory so the directory will be separated from
// one created through the regular Context
return getContext().getDir("mockcontext2_" + name, mode);
}
@Override
public Context getApplicationContext() {
return this;
}
}
@Override
public void setUp() throws Exception {
super.setUp();
mMockResolver = new MockContentResolver();
final String filenamePrefix = "test.";
RenamingDelegatingContext targetContextWrapper = new
RenamingDelegatingContext(
new MockContext2(), // The context that most methods are delegated to
getContext(), // The context that file methods are delegated to
filenamePrefix);
mMockContext = new IsolatedContext(mMockResolver, targetContextWrapper);
mMockResolver.addProvider(MockProvider.AUTHORITY, new MockProvider(mMockContext));
}
public CalendarSyncAdapterTests() {
super();
}
public void testSetTimeRelatedValues_NonRecurring() throws IOException {
CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
ContentValues cv = new ContentValues();
// Basic, one-time meeting lasting an hour
GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
Long startTime = startCalendar.getTimeInMillis();
GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30);
Long endTime = endCalendar.getTimeInMillis();
p.setTimeRelatedValues(cv, startTime, endTime, 0);
assertNull(cv.getAsInteger(Events.DURATION));
assertEquals(startTime, cv.getAsLong(Events.DTSTART));
assertEquals(endTime, cv.getAsLong(Events.DTEND));
assertEquals(endTime, cv.getAsLong(Events.LAST_DATE));
assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
}
public void testSetTimeRelatedValues_Recurring() throws IOException {
CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
ContentValues cv = new ContentValues();
// Recurring meeting lasting an hour
GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
Long startTime = startCalendar.getTimeInMillis();
GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30);
Long endTime = endCalendar.getTimeInMillis();
cv.put(Events.RRULE, "FREQ=DAILY");
p.setTimeRelatedValues(cv, startTime, endTime, 0);
assertEquals("P60M", cv.getAsString(Events.DURATION));
assertEquals(startTime, cv.getAsLong(Events.DTSTART));
assertNull(cv.getAsLong(Events.DTEND));
assertNull(cv.getAsLong(Events.LAST_DATE));
assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
}
public void testSetTimeRelatedValues_AllDay() throws IOException {
CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
ContentValues cv = new ContentValues();
GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
Long startTime = startCalendar.getTimeInMillis();
GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 11, 8, 30);
Long endTime = endCalendar.getTimeInMillis();
cv.put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO");
p.setTimeRelatedValues(cv, startTime, endTime, 1);
// The start time should have hour/min/sec zero'd out
startCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
startCalendar.set(2010, 5, 10, 0, 0, 0);
startCalendar.set(GregorianCalendar.MILLISECOND, 0);
startTime = startCalendar.getTimeInMillis();
assertEquals(startTime, cv.getAsLong(Events.DTSTART));
// The duration should be in days
assertEquals("P1D", cv.getAsString(Events.DURATION));
assertNull(cv.getAsLong(Events.DTEND));
assertNull(cv.getAsLong(Events.LAST_DATE));
// There must be a timezone
assertNotNull(cv.getAsString(Events.EVENT_TIMEZONE));
}
public void testSetTimeRelatedValues_Recurring_AllDay_Exception () throws IOException {
CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
ContentValues cv = new ContentValues();
// Recurrence exception for all-day event; the exception is NOT all-day
GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 17, 8, 30);
Long startTime = startCalendar.getTimeInMillis();
GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 17, 9, 30);
Long endTime = endCalendar.getTimeInMillis();
cv.put(Events.ORIGINAL_ALL_DAY, 1);
GregorianCalendar instanceCalendar = new GregorianCalendar(2010, 5, 17, 8, 30);
cv.put(Events.ORIGINAL_INSTANCE_TIME, instanceCalendar.getTimeInMillis());
p.setTimeRelatedValues(cv, startTime, endTime, 0);
// The original instance time should have hour/min/sec zero'd out
GregorianCalendar testCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
testCalendar.set(2010, 5, 17, 0, 0, 0);
testCalendar.set(GregorianCalendar.MILLISECOND, 0);
Long testTime = testCalendar.getTimeInMillis();
assertEquals(testTime, cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
// The exception isn't all-day, so we should have DTEND and LAST_DATE and no EVENT_TIMEZONE
assertNull(cv.getAsString(Events.DURATION));
assertEquals(endTime, cv.getAsLong(Events.DTEND));
assertEquals(endTime, cv.getAsLong(Events.LAST_DATE));
assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
}
public void testIsValidEventValues() throws IOException {
CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
long validTime = System.currentTimeMillis();
String validData = "foo-bar-bletch";
String validDuration = "P30M";
String validRrule = "FREQ=DAILY";
ContentValues cv = new ContentValues();
cv.put(Events.DTSTART, validTime);
// Needs _SYNC_DATA and DTEND/DURATION
assertFalse(p.isValidEventValues(cv));
cv.put(Events.SYNC_DATA2, validData);
// Needs DTEND/DURATION since not an exception
assertFalse(p.isValidEventValues(cv));
cv.put(Events.DURATION, validDuration);
// Valid (DTSTART, _SYNC_DATA, DURATION)
assertTrue(p.isValidEventValues(cv));
cv.remove(Events.DURATION);
cv.put(Events.ORIGINAL_INSTANCE_TIME, validTime);
// Needs DTEND since it's an exception
assertFalse(p.isValidEventValues(cv));
cv.put(Events.DTEND, validTime);
// Valid (DTSTART, DTEND, ORIGINAL_INSTANCE_TIME)
cv.remove(Events.ORIGINAL_INSTANCE_TIME);
// Valid (DTSTART, _SYNC_DATA, DTEND)
assertTrue(p.isValidEventValues(cv));
cv.remove(Events.DTSTART);
// Needs DTSTART
assertFalse(p.isValidEventValues(cv));
cv.put(Events.DTSTART, validTime);
cv.put(Events.RRULE, validRrule);
// With RRULE, needs DURATION
assertFalse(p.isValidEventValues(cv));
cv.put(Events.DURATION, "P30M");
// Valid (DTSTART, RRULE, DURATION)
assertTrue(p.isValidEventValues(cv));
cv.put(Events.ALL_DAY, "1");
// Needs DURATION in the form P<n>D
assertFalse(p.isValidEventValues(cv));
// Valid (DTSTART, RRULE, ALL_DAY, DURATION(P<n>D)
cv.put(Events.DURATION, "P1D");
assertTrue(p.isValidEventValues(cv));
}
private void addAttendeesToSerializer(Serializer s, int num) throws IOException {
for (int i = 0; i < num; i++) {
s.start(Tags.CALENDAR_ATTENDEE);
s.data(Tags.CALENDAR_ATTENDEE_EMAIL, "frederick" + num +
".flintstone@this.that.verylongservername.com");
s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
s.data(Tags.CALENDAR_ATTENDEE_NAME, "Frederick" + num + " Flintstone, III");
s.end();
}
}
private void addAttendeeToSerializer(Serializer s, String email, String name)
throws IOException {
s.start(Tags.CALENDAR_ATTENDEE);
s.data(Tags.CALENDAR_ATTENDEE_EMAIL, email);
s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
s.data(Tags.CALENDAR_ATTENDEE_NAME, name);
s.end();
}
private int countInsertOperationsForTable(CalendarOperations ops, String tableName) {
int cnt = 0;
for (Operation op: ops) {
ContentProviderOperation cpo =
AbstractSyncAdapter.operationToContentProviderOperation(op, 0);
List<String> segments = cpo.getUri().getPathSegments();
if (segments.get(0).equalsIgnoreCase(tableName) &&
cpo.getType() == ContentProviderOperation.TYPE_INSERT) {
cnt++;
}
}
return cnt;
}
class TestEvent extends Serializer {
CalendarSyncAdapter mAdapter;
EasCalendarSyncParser mParser;
Serializer mSerializer;
TestEvent() throws IOException {
super(false);
mAdapter = getTestSyncAdapter(CalendarSyncAdapter.class);
mParser = mAdapter.new EasCalendarSyncParser(getTestInputStream(), mAdapter);
}
void setUserEmailAddress(String addr) {
mAdapter.mAccount.mEmailAddress = addr;
mAdapter.mEmailAddress = addr;
}
EasCalendarSyncParser getParser() throws IOException {
// Set up our parser's input and eat the initial tag
mParser.resetInput(new ByteArrayInputStream(toByteArray()));
mParser.nextTag(0);
return mParser;
}
// setupPreAttendees and setupPostAttendees initialize calendar data in the order in which
// they would appear in an actual EAS session. Between these two calls, we initialize
// attendee data, which varies between the following tests
TestEvent setupPreAttendees() throws IOException {
start(Tags.SYNC_APPLICATION_DATA);
data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
data(Tags.CALENDAR_SUBJECT, "Documentation");
data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
return this;
}
TestEvent setupPostAttendees()throws IOException {
data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
start(Tags.BASE_BODY);
data(Tags.BASE_BODY_PREFERENCE, "1");
data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
data(Tags.BASE_DATA,
"This is the event description; we should probably make it longer");
end(); // BASE_BODY
start(Tags.CALENDAR_RECURRENCE);
data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); // weekly
data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, "10");
data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "12"); // tue, wed
data(Tags.CALENDAR_RECURRENCE_UNTIL, "2005-04-14T00:00:00.000Z");
end(); // CALENDAR_RECURRENCE
data(Tags.CALENDAR_SENSITIVITY, "0");
data(Tags.CALENDAR_BUSY_STATUS, "2");
data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
data(Tags.CALENDAR_MEETING_STATUS, "3");
data(Tags.BASE_NATIVE_BODY_TYPE, "3");
end().done(); // SYNC_APPLICATION_DATA
return this;
}
}
public void testAddEvent() throws IOException {
TestEvent event = new TestEvent();
event.setupPreAttendees();
event.start(Tags.CALENDAR_ATTENDEES);
addAttendeesToSerializer(event, 10);
event.end(); // CALENDAR_ATTENDEES
event.setupPostAttendees();
EasCalendarSyncParser p = event.getParser();
p.addEvent(p.mOps, "1:1", false);
// There should be 1 event
assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
// Two attendees (organizer and 10 attendees)
assertEquals(11, countInsertOperationsForTable(p.mOps, "attendees"));
// dtstamp, meeting status, attendees, attendees redacted, and upsync prohibited
assertEquals(5, countInsertOperationsForTable(p.mOps, "extendedproperties"));
}
public void testAddEventIllegal() throws IOException {
// We don't send a start time; the event is illegal and nothing should be added
TestEvent event = new TestEvent();
event.start(Tags.SYNC_APPLICATION_DATA);
event.data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
event.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
event.data(Tags.CALENDAR_SUBJECT, "Documentation");
event.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
event.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
event.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
event.start(Tags.CALENDAR_ATTENDEES);
addAttendeesToSerializer(event, 10);
event.end(); // CALENDAR_ATTENDEES
event.setupPostAttendees();
EasCalendarSyncParser p = event.getParser();
p.addEvent(p.mOps, "1:1", false);
assertEquals(0, countInsertOperationsForTable(p.mOps, "events"));
assertEquals(0, countInsertOperationsForTable(p.mOps, "attendees"));
assertEquals(0, countInsertOperationsForTable(p.mOps, "extendedproperties"));
}
public void testAddEventRedactedAttendees() throws IOException {
TestEvent event = new TestEvent();
event.setupPreAttendees();
event.start(Tags.CALENDAR_ATTENDEES);
addAttendeesToSerializer(event, 100);
event.end(); // CALENDAR_ATTENDEES
event.setupPostAttendees();
EasCalendarSyncParser p = event.getParser();
p.addEvent(p.mOps, "1:1", false);
// There should be 1 event
assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
// One attendees (organizer; all others are redacted)
assertEquals(1, countInsertOperationsForTable(p.mOps, "attendees"));
// dtstamp, meeting status, and attendees redacted
assertEquals(3, countInsertOperationsForTable(p.mOps, "extendedproperties"));
}
/**
* Setup for the following three tests, which check attendee status of an added event
* @param userEmail the email address of the user
* @param update whether or not the event is an update (rather than new)
* @return a Cursor to the Attendee records added to our MockProvider
* @throws IOException
* @throws RemoteException
* @throws OperationApplicationException
*/
private Cursor setupAddEventOneAttendee(String userEmail, boolean update)
throws IOException, RemoteException, OperationApplicationException {
TestEvent event = new TestEvent();
event.setupPreAttendees();
event.start(Tags.CALENDAR_ATTENDEES);
addAttendeeToSerializer(event, SINGLE_ATTENDEE_EMAIL, SINGLE_ATTENDEE_NAME);
event.setUserEmailAddress(userEmail);
event.end(); // CALENDAR_ATTENDEES
event.setupPostAttendees();
EasCalendarSyncParser p = event.getParser();
p.addEvent(p.mOps, "1:1", update);
// Send the CPO's to the mock provider
ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
for (Operation op: p.mOps) {
cpos.add(AbstractSyncAdapter.operationToContentProviderOperation(op, 0));
}
mMockResolver.applyBatch(MockProvider.AUTHORITY, cpos);
return mMockResolver.query(MockProvider.uri(Attendees.CONTENT_URI), ATTENDEE_PROJECTION,
null, null, null);
}
public void testAddEventOneAttendee() throws IOException, RemoteException,
OperationApplicationException {
Cursor c = setupAddEventOneAttendee("foo@bar.com", false);
assertEquals(2, c.getCount());
// The organizer should be "accepted", the unknown attendee "none"
while (c.moveToNext()) {
if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
} else {
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
}
}
}
public void testAddEventSelfAttendee() throws IOException, RemoteException,
OperationApplicationException {
Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, false);
// The organizer should be "accepted", and our user/attendee should be "done" even though
// the busy status = 2 (because we can't tell from a status of 2 on new events)
while (c.moveToNext()) {
if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
} else {
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
}
}
}
public void testAddEventSelfAttendeeUpdate() throws IOException, RemoteException,
OperationApplicationException {
Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, true);
// The organizer should be "accepted", and our user/attendee should be "accepted" (because
// busy status = 2 and this is an update
while (c.moveToNext()) {
if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
} else {
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
}
}
}
}