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