CalendarSyncAdapterTests.java revision c8e4352ea6cfa67f15140512e84af8ccede222d2
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.exchange.adapter;
18
19import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations;
20import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
21import com.android.exchange.provider.MockProvider;
22
23import android.content.ContentProviderOperation;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.OperationApplicationException;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.os.RemoteException;
30import android.provider.Calendar.Attendees;
31import android.provider.Calendar.Events;
32import android.test.IsolatedContext;
33import android.test.RenamingDelegatingContext;
34import android.test.mock.MockContentResolver;
35import android.test.mock.MockContext;
36
37import java.io.ByteArrayInputStream;
38import java.io.File;
39import java.io.IOException;
40import java.util.GregorianCalendar;
41import java.util.List;
42import java.util.TimeZone;
43
44/**
45 * You can run this entire test case with:
46 *   runtest -c com.android.exchange.adapter.CalendarSyncAdapterTests exchange
47 */
48
49public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
50    private static final String[] ATTENDEE_PROJECTION = new String[] {Attendees.ATTENDEE_EMAIL,
51            Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_STATUS};
52    private static final int ATTENDEE_EMAIL = 0;
53    private static final int ATTENDEE_NAME = 1;
54    private static final int ATTENDEE_STATUS = 2;
55
56    private static final String SINGLE_ATTENDEE_EMAIL = "attendee@host.com";
57    private static final String SINGLE_ATTENDEE_NAME = "Bill Attendee";
58
59    private Context mMockContext;
60    private MockContentResolver mMockResolver;
61
62    // This is the US/Pacific time zone as a base64-encoded TIME_ZONE_INFORMATION structure, as
63    // it would appear coming from an Exchange server
64    private static final String TEST_TIME_ZONE = "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
65        "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
66        "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
67        "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
68
69    private class MockContext2 extends MockContext {
70
71        @Override
72        public Resources getResources() {
73            return getContext().getResources();
74        }
75
76        @Override
77        public File getDir(String name, int mode) {
78            // name the directory so the directory will be separated from
79            // one created through the regular Context
80            return getContext().getDir("mockcontext2_" + name, mode);
81        }
82
83        @Override
84        public Context getApplicationContext() {
85            return this;
86        }
87    }
88
89    @Override
90    public void setUp() throws Exception {
91        super.setUp();
92
93        mMockResolver = new MockContentResolver();
94        final String filenamePrefix = "test.";
95        RenamingDelegatingContext targetContextWrapper = new
96        RenamingDelegatingContext(
97                new MockContext2(), // The context that most methods are delegated to
98                getContext(), // The context that file methods are delegated to
99                filenamePrefix);
100        mMockContext = new IsolatedContext(mMockResolver, targetContextWrapper);
101        mMockResolver.addProvider(MockProvider.AUTHORITY, new MockProvider(mMockContext));
102    }
103
104    public CalendarSyncAdapterTests() {
105        super();
106    }
107
108    public void testSetTimeRelatedValues_NonRecurring() throws IOException {
109        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
110        EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
111        ContentValues cv = new ContentValues();
112        // Basic, one-time meeting lasting an hour
113        GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
114        Long startTime = startCalendar.getTimeInMillis();
115        GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30);
116        Long endTime = endCalendar.getTimeInMillis();
117
118        p.setTimeRelatedValues(cv, startTime, endTime, 0);
119        assertNull(cv.getAsInteger(Events.DURATION));
120        assertEquals(startTime, cv.getAsLong(Events.DTSTART));
121        assertEquals(endTime, cv.getAsLong(Events.DTEND));
122        assertEquals(endTime, cv.getAsLong(Events.LAST_DATE));
123        assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
124    }
125
126    public void testSetTimeRelatedValues_Recurring() throws IOException {
127        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
128        EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
129        ContentValues cv = new ContentValues();
130        // Recurring meeting lasting an hour
131        GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
132        Long startTime = startCalendar.getTimeInMillis();
133        GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30);
134        Long endTime = endCalendar.getTimeInMillis();
135        cv.put(Events.RRULE, "FREQ=DAILY");
136        p.setTimeRelatedValues(cv, startTime, endTime, 0);
137        assertEquals("P60M", cv.getAsString(Events.DURATION));
138        assertEquals(startTime, cv.getAsLong(Events.DTSTART));
139        assertNull(cv.getAsLong(Events.DTEND));
140        assertNull(cv.getAsLong(Events.LAST_DATE));
141        assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
142    }
143
144    public void testSetTimeRelatedValues_AllDay() throws IOException {
145        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
146        EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
147        ContentValues cv = new ContentValues();
148        GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
149        Long startTime = startCalendar.getTimeInMillis();
150        GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 11, 8, 30);
151        Long endTime = endCalendar.getTimeInMillis();
152        cv.put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO");
153        p.setTimeRelatedValues(cv, startTime, endTime, 1);
154
155        // The start time should have hour/min/sec zero'd out
156        startCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
157        startCalendar.set(2010, 5, 10, 0, 0, 0);
158        startCalendar.set(GregorianCalendar.MILLISECOND, 0);
159        startTime = startCalendar.getTimeInMillis();
160        assertEquals(startTime, cv.getAsLong(Events.DTSTART));
161
162        // The duration should be in days
163        assertEquals("P1D", cv.getAsString(Events.DURATION));
164        assertNull(cv.getAsLong(Events.DTEND));
165        assertNull(cv.getAsLong(Events.LAST_DATE));
166        // There must be a timezone
167        assertNotNull(cv.getAsString(Events.EVENT_TIMEZONE));
168    }
169
170    public void testSetTimeRelatedValues_Recurring_AllDay_Exception () throws IOException {
171        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
172        EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
173        ContentValues cv = new ContentValues();
174
175        // Recurrence exception for all-day event; the exception is NOT all-day
176        GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 17, 8, 30);
177        Long startTime = startCalendar.getTimeInMillis();
178        GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 17, 9, 30);
179        Long endTime = endCalendar.getTimeInMillis();
180        cv.put(Events.ORIGINAL_ALL_DAY, 1);
181        GregorianCalendar instanceCalendar = new GregorianCalendar(2010, 5, 17, 8, 30);
182        cv.put(Events.ORIGINAL_INSTANCE_TIME, instanceCalendar.getTimeInMillis());
183        p.setTimeRelatedValues(cv, startTime, endTime, 0);
184
185        // The original instance time should have hour/min/sec zero'd out
186        GregorianCalendar testCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
187        testCalendar.set(2010, 5, 17, 0, 0, 0);
188        testCalendar.set(GregorianCalendar.MILLISECOND, 0);
189        Long testTime = testCalendar.getTimeInMillis();
190        assertEquals(testTime, cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
191
192        // The exception isn't all-day, so we should have DTEND and LAST_DATE and no EVENT_TIMEZONE
193        assertNull(cv.getAsString(Events.DURATION));
194        assertEquals(endTime, cv.getAsLong(Events.DTEND));
195        assertEquals(endTime, cv.getAsLong(Events.LAST_DATE));
196        assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
197    }
198
199    public void testIsValidEventValues() throws IOException {
200        CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
201        EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
202
203        long validTime = System.currentTimeMillis();
204        String validData = "foo-bar-bletch";
205        String validDuration = "P30M";
206        String validRrule = "FREQ=DAILY";
207
208        ContentValues cv = new ContentValues();
209
210        cv.put(Events.DTSTART, validTime);
211        // Needs _SYNC_DATA and DTEND/DURATION
212        assertFalse(p.isValidEventValues(cv));
213        cv.put(Events._SYNC_DATA, validData);
214        // Needs DTEND/DURATION since not an exception
215        assertFalse(p.isValidEventValues(cv));
216        cv.put(Events.DURATION, validDuration);
217        // Valid (DTSTART, _SYNC_DATA, DURATION)
218        assertTrue(p.isValidEventValues(cv));
219        cv.remove(Events.DURATION);
220        cv.put(Events.ORIGINAL_INSTANCE_TIME, validTime);
221        // Needs DTEND since it's an exception
222        assertFalse(p.isValidEventValues(cv));
223        cv.put(Events.DTEND, validTime);
224        // Valid (DTSTART, DTEND, ORIGINAL_INSTANCE_TIME)
225        cv.remove(Events.ORIGINAL_INSTANCE_TIME);
226        // Valid (DTSTART, _SYNC_DATA, DTEND)
227        assertTrue(p.isValidEventValues(cv));
228        cv.remove(Events.DTSTART);
229        // Needs DTSTART
230        assertFalse(p.isValidEventValues(cv));
231        cv.put(Events.DTSTART, validTime);
232        cv.put(Events.RRULE, validRrule);
233        // With RRULE, needs DURATION
234        assertFalse(p.isValidEventValues(cv));
235        cv.put(Events.DURATION, "P30M");
236        // Valid (DTSTART, RRULE, DURATION)
237        assertTrue(p.isValidEventValues(cv));
238        cv.put(Events.ALL_DAY, "1");
239        // Needs DURATION in the form P<n>D
240        assertFalse(p.isValidEventValues(cv));
241        // Valid (DTSTART, RRULE, ALL_DAY, DURATION(P<n>D)
242        cv.put(Events.DURATION, "P1D");
243        assertTrue(p.isValidEventValues(cv));
244    }
245
246    private void addAttendeesToSerializer(Serializer s, int num) throws IOException {
247        for (int i = 0; i < num; i++) {
248            s.start(Tags.CALENDAR_ATTENDEE);
249            s.data(Tags.CALENDAR_ATTENDEE_EMAIL, "frederick" + num +
250                    ".flintstone@this.that.verylongservername.com");
251            s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
252            s.data(Tags.CALENDAR_ATTENDEE_NAME, "Frederick" + num + " Flintstone, III");
253            s.end();
254        }
255    }
256
257    private void addAttendeeToSerializer(Serializer s, String email, String name)
258            throws IOException {
259        s.start(Tags.CALENDAR_ATTENDEE);
260        s.data(Tags.CALENDAR_ATTENDEE_EMAIL, email);
261        s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
262        s.data(Tags.CALENDAR_ATTENDEE_NAME, name);
263        s.end();
264    }
265
266    private int countInsertOperationsForTable(CalendarOperations ops, String tableName) {
267        int cnt = 0;
268        for (ContentProviderOperation op: ops) {
269            List<String> segments = op.getUri().getPathSegments();
270            if (segments.get(0).equalsIgnoreCase(tableName) &&
271                    op.getType() == ContentProviderOperation.TYPE_INSERT) {
272                cnt++;
273            }
274        }
275        return cnt;
276    }
277
278    class TestEvent extends Serializer {
279        CalendarSyncAdapter mAdapter;
280        EasCalendarSyncParser mParser;
281        Serializer mSerializer;
282
283        TestEvent() throws IOException {
284            super(false);
285            mAdapter = getTestSyncAdapter(CalendarSyncAdapter.class);
286            mParser = mAdapter.new EasCalendarSyncParser(getTestInputStream(), mAdapter);
287        }
288
289        void setUserEmailAddress(String addr) {
290            mAdapter.mAccount.mEmailAddress = addr;
291            mAdapter.mEmailAddress = addr;
292        }
293
294        EasCalendarSyncParser getParser() throws IOException {
295            // Set up our parser's input and eat the initial tag
296            mParser.resetInput(new ByteArrayInputStream(toByteArray()));
297            mParser.nextTag(0);
298            return mParser;
299        }
300
301        // setupPreAttendees and setupPostAttendees initialize calendar data in the order in which
302        // they would appear in an actual EAS session.  Between these two calls, we initialize
303        // attendee data, which varies between the following tests
304        TestEvent setupPreAttendees() throws IOException {
305            start(Tags.SYNC_APPLICATION_DATA);
306            data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
307            data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
308            data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
309            data(Tags.CALENDAR_SUBJECT, "Documentation");
310            data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
311            data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
312            data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
313            return this;
314        }
315
316        TestEvent setupPostAttendees()throws IOException {
317            data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
318            data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
319            start(Tags.BASE_BODY);
320            data(Tags.BASE_BODY_PREFERENCE, "1");
321            data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
322            data(Tags.BASE_DATA,
323                    "This is the event description; we should probably make it longer");
324            end(); // BASE_BODY
325            start(Tags.CALENDAR_RECURRENCE);
326            data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); // weekly
327            data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
328            data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, "10");
329            data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "12"); // tue, wed
330            data(Tags.CALENDAR_RECURRENCE_UNTIL, "2005-04-14T00:00:00.000Z");
331            end();  // CALENDAR_RECURRENCE
332            data(Tags.CALENDAR_SENSITIVITY, "0");
333            data(Tags.CALENDAR_BUSY_STATUS, "2");
334            data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
335            data(Tags.CALENDAR_MEETING_STATUS, "3");
336            data(Tags.BASE_NATIVE_BODY_TYPE, "3");
337            end().done(); // SYNC_APPLICATION_DATA
338            return this;
339        }
340    }
341
342    public void testAddEvent() throws IOException {
343        TestEvent event = new TestEvent();
344        event.setupPreAttendees();
345        event.start(Tags.CALENDAR_ATTENDEES);
346        addAttendeesToSerializer(event, 10);
347        event.end(); // CALENDAR_ATTENDEES
348        event.setupPostAttendees();
349
350        EasCalendarSyncParser p = event.getParser();
351        p.addEvent(p.mOps, "1:1", false);
352        // There should be 1 event
353        assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
354        // Two attendees (organizer and 10 attendees)
355        assertEquals(11, countInsertOperationsForTable(p.mOps, "attendees"));
356        // dtstamp, meeting status, attendees, attendees redacted, and upsync prohibited
357        assertEquals(5, countInsertOperationsForTable(p.mOps, "extendedproperties"));
358    }
359
360    public void testAddEventIllegal() throws IOException {
361        // We don't send a start time; the event is illegal and nothing should be added
362        TestEvent event = new TestEvent();
363        event.start(Tags.SYNC_APPLICATION_DATA);
364        event.data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
365        event.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
366        event.data(Tags.CALENDAR_SUBJECT, "Documentation");
367        event.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
368        event.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
369        event.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
370        event.start(Tags.CALENDAR_ATTENDEES);
371        addAttendeesToSerializer(event, 10);
372        event.end(); // CALENDAR_ATTENDEES
373        event.setupPostAttendees();
374
375        EasCalendarSyncParser p = event.getParser();
376        p.addEvent(p.mOps, "1:1", false);
377        assertEquals(0, countInsertOperationsForTable(p.mOps, "events"));
378        assertEquals(0, countInsertOperationsForTable(p.mOps, "attendees"));
379        assertEquals(0, countInsertOperationsForTable(p.mOps, "extendedproperties"));
380    }
381
382    public void testAddEventRedactedAttendees() throws IOException {
383        TestEvent event = new TestEvent();
384        event.setupPreAttendees();
385        event.start(Tags.CALENDAR_ATTENDEES);
386        addAttendeesToSerializer(event, 100);
387        event.end(); // CALENDAR_ATTENDEES
388        event.setupPostAttendees();
389
390        EasCalendarSyncParser p = event.getParser();
391        p.addEvent(p.mOps, "1:1", false);
392        // There should be 1 event
393        assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
394        // One attendees (organizer; all others are redacted)
395        assertEquals(1, countInsertOperationsForTable(p.mOps, "attendees"));
396        // dtstamp, meeting status, and attendees redacted
397        assertEquals(3, countInsertOperationsForTable(p.mOps, "extendedproperties"));
398    }
399
400    /**
401     * Setup for the following three tests, which check attendee status of an added event
402     * @param userEmail the email address of the user
403     * @param update whether or not the event is an update (rather than new)
404     * @return a Cursor to the Attendee records added to our MockProvider
405     * @throws IOException
406     * @throws RemoteException
407     * @throws OperationApplicationException
408     */
409    private Cursor setupAddEventOneAttendee(String userEmail, boolean update)
410            throws IOException, RemoteException, OperationApplicationException {
411        TestEvent event = new TestEvent();
412        event.setupPreAttendees();
413        event.start(Tags.CALENDAR_ATTENDEES);
414        addAttendeeToSerializer(event, SINGLE_ATTENDEE_EMAIL, SINGLE_ATTENDEE_NAME);
415        event.setUserEmailAddress(userEmail);
416        event.end(); // CALENDAR_ATTENDEES
417        event.setupPostAttendees();
418
419        EasCalendarSyncParser p = event.getParser();
420        p.addEvent(p.mOps, "1:1", update);
421        // Send the CPO's to the mock provider
422        mMockResolver.applyBatch(MockProvider.AUTHORITY, p.mOps);
423        return mMockResolver.query(MockProvider.uri(Attendees.CONTENT_URI), ATTENDEE_PROJECTION,
424                null, null, null);
425    }
426
427    public void testAddEventOneAttendee() throws IOException, RemoteException,
428            OperationApplicationException {
429        Cursor c = setupAddEventOneAttendee("foo@bar.com", false);
430        assertEquals(2, c.getCount());
431        // The organizer should be "accepted", the unknown attendee "none"
432        while (c.moveToNext()) {
433            if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
434                assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
435            } else {
436                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
437            }
438        }
439    }
440
441    public void testAddEventSelfAttendee() throws IOException, RemoteException,
442            OperationApplicationException {
443        Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, false);
444        // The organizer should be "accepted", and our user/attendee should be "done" even though
445        // the busy status = 2 (because we can't tell from a status of 2 on new events)
446        while (c.moveToNext()) {
447            if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
448                assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
449            } else {
450                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
451            }
452        }
453    }
454
455    public void testAddEventSelfAttendeeUpdate() throws IOException, RemoteException,
456            OperationApplicationException {
457        Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, true);
458        // The organizer should be "accepted", and our user/attendee should be "accepted" (because
459        // busy status = 2 and this is an update
460        while (c.moveToNext()) {
461            if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
462                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
463            } else {
464                assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
465            }
466        }
467    }
468}
469