EditEventHelper.java revision 09fbd8e9ef61f667c0f20d36fbf40e5a4479c8d9
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.calendar.event;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.graphics.drawable.Drawable;
25import android.net.Uri;
26import android.provider.CalendarContract.Attendees;
27import android.provider.CalendarContract.Calendars;
28import android.provider.CalendarContract.Events;
29import android.provider.CalendarContract.Reminders;
30import android.text.TextUtils;
31import android.text.format.DateUtils;
32import android.text.format.Time;
33import android.text.util.Rfc822Token;
34import android.text.util.Rfc822Tokenizer;
35import android.util.Log;
36import android.view.View;
37
38import com.android.calendar.AbstractCalendarActivity;
39import com.android.calendar.AsyncQueryService;
40import com.android.calendar.CalendarEventModel;
41import com.android.calendar.CalendarEventModel.Attendee;
42import com.android.calendar.CalendarEventModel.ReminderEntry;
43import com.android.calendar.Utils;
44import com.android.calendarcommon2.DateException;
45import com.android.calendarcommon2.EventRecurrence;
46import com.android.calendarcommon2.RecurrenceProcessor;
47import com.android.calendarcommon2.RecurrenceSet;
48import com.android.common.Rfc822Validator;
49
50import java.util.ArrayList;
51import java.util.HashMap;
52import java.util.Iterator;
53import java.util.LinkedHashSet;
54import java.util.LinkedList;
55import java.util.TimeZone;
56
57public class EditEventHelper {
58    private static final String TAG = "EditEventHelper";
59
60    private static final boolean DEBUG = false;
61
62    public static final String[] EVENT_PROJECTION = new String[] {
63            Events._ID, // 0
64            Events.TITLE, // 1
65            Events.DESCRIPTION, // 2
66            Events.EVENT_LOCATION, // 3
67            Events.ALL_DAY, // 4
68            Events.HAS_ALARM, // 5
69            Events.CALENDAR_ID, // 6
70            Events.DTSTART, // 7
71            Events.DTEND, // 8
72            Events.DURATION, // 9
73            Events.EVENT_TIMEZONE, // 10
74            Events.RRULE, // 11
75            Events._SYNC_ID, // 12
76            Events.AVAILABILITY, // 13
77            Events.ACCESS_LEVEL, // 14
78            Events.OWNER_ACCOUNT, // 15
79            Events.HAS_ATTENDEE_DATA, // 16
80            Events.ORIGINAL_SYNC_ID, // 17
81            Events.ORGANIZER, // 18
82            Events.GUESTS_CAN_MODIFY, // 19
83            Events.ORIGINAL_ID, // 20
84            Events.STATUS, // 21
85    };
86    protected static final int EVENT_INDEX_ID = 0;
87    protected static final int EVENT_INDEX_TITLE = 1;
88    protected static final int EVENT_INDEX_DESCRIPTION = 2;
89    protected static final int EVENT_INDEX_EVENT_LOCATION = 3;
90    protected static final int EVENT_INDEX_ALL_DAY = 4;
91    protected static final int EVENT_INDEX_HAS_ALARM = 5;
92    protected static final int EVENT_INDEX_CALENDAR_ID = 6;
93    protected static final int EVENT_INDEX_DTSTART = 7;
94    protected static final int EVENT_INDEX_DTEND = 8;
95    protected static final int EVENT_INDEX_DURATION = 9;
96    protected static final int EVENT_INDEX_TIMEZONE = 10;
97    protected static final int EVENT_INDEX_RRULE = 11;
98    protected static final int EVENT_INDEX_SYNC_ID = 12;
99    protected static final int EVENT_INDEX_AVAILABILITY = 13;
100    protected static final int EVENT_INDEX_ACCESS_LEVEL = 14;
101    protected static final int EVENT_INDEX_OWNER_ACCOUNT = 15;
102    protected static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 16;
103    protected static final int EVENT_INDEX_ORIGINAL_SYNC_ID = 17;
104    protected static final int EVENT_INDEX_ORGANIZER = 18;
105    protected static final int EVENT_INDEX_GUESTS_CAN_MODIFY = 19;
106    protected static final int EVENT_INDEX_ORIGINAL_ID = 20;
107    protected static final int EVENT_INDEX_EVENT_STATUS = 21;
108
109    public static final String[] REMINDERS_PROJECTION = new String[] {
110            Reminders._ID, // 0
111            Reminders.MINUTES, // 1
112            Reminders.METHOD, // 2
113    };
114    public static final int REMINDERS_INDEX_MINUTES = 1;
115    public static final int REMINDERS_INDEX_METHOD = 2;
116    public static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?";
117
118    // Visible for testing
119    static final String ATTENDEES_DELETE_PREFIX = Attendees.EVENT_ID + "=? AND "
120            + Attendees.ATTENDEE_EMAIL + " IN (";
121
122    public static final int DOES_NOT_REPEAT = 0;
123    public static final int REPEATS_DAILY = 1;
124    public static final int REPEATS_EVERY_WEEKDAY = 2;
125    public static final int REPEATS_WEEKLY_ON_DAY = 3;
126    public static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
127    public static final int REPEATS_MONTHLY_ON_DAY = 5;
128    public static final int REPEATS_YEARLY = 6;
129    public static final int REPEATS_CUSTOM = 7;
130
131    protected static final int MODIFY_UNINITIALIZED = 0;
132    protected static final int MODIFY_SELECTED = 1;
133    protected static final int MODIFY_ALL_FOLLOWING = 2;
134    protected static final int MODIFY_ALL = 3;
135
136    protected static final int DAY_IN_SECONDS = 24 * 60 * 60;
137
138    private final AsyncQueryService mService;
139
140    // This allows us to flag the event if something is wrong with it, right now
141    // if an uri is provided for an event that doesn't exist in the db.
142    protected boolean mEventOk = true;
143
144    public static final int ATTENDEE_ID_NONE = -1;
145    public static final int[] ATTENDEE_VALUES = {
146        Attendees.ATTENDEE_STATUS_NONE,
147        Attendees.ATTENDEE_STATUS_ACCEPTED,
148        Attendees.ATTENDEE_STATUS_TENTATIVE,
149        Attendees.ATTENDEE_STATUS_DECLINED,
150    };
151
152    /**
153     * This is the symbolic name for the key used to pass in the boolean for
154     * creating all-day events that is part of the extra data of the intent.
155     * This is used only for creating new events and is set to true if the
156     * default for the new event should be an all-day event.
157     */
158    public static final String EVENT_ALL_DAY = "allDay";
159
160    static final String[] CALENDARS_PROJECTION = new String[] {
161            Calendars._ID, // 0
162            Calendars.CALENDAR_DISPLAY_NAME, // 1
163            Calendars.OWNER_ACCOUNT, // 2
164            Calendars.CALENDAR_COLOR, // 3
165            Calendars.CAN_ORGANIZER_RESPOND, // 4
166            Calendars.CALENDAR_ACCESS_LEVEL, // 5
167            Calendars.VISIBLE, // 6
168            Calendars.MAX_REMINDERS, // 7
169            Calendars.ALLOWED_REMINDERS, // 8
170            Calendars.ALLOWED_ATTENDEE_TYPES, // 9
171            Calendars.ALLOWED_AVAILABILITY, // 10
172            Calendars.ACCOUNT_NAME, // 11
173            Calendars.ACCOUNT_TYPE, //12
174    };
175    static final int CALENDARS_INDEX_ID = 0;
176    static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
177    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
178    static final int CALENDARS_INDEX_COLOR = 3;
179    static final int CALENDARS_INDEX_CAN_ORGANIZER_RESPOND = 4;
180    static final int CALENDARS_INDEX_ACCESS_LEVEL = 5;
181    static final int CALENDARS_INDEX_VISIBLE = 6;
182    static final int CALENDARS_INDEX_MAX_REMINDERS = 7;
183    static final int CALENDARS_INDEX_ALLOWED_REMINDERS = 8;
184    static final int CALENDARS_INDEX_ALLOWED_ATTENDEE_TYPES = 9;
185    static final int CALENDARS_INDEX_ALLOWED_AVAILABILITY = 10;
186    static final int CALENDARS_INDEX_ACCOUNT_NAME = 11;
187    static final int CALENDARS_INDEX_ACCOUNT_TYPE = 12;
188
189    static final String CALENDARS_WHERE_WRITEABLE_VISIBLE = Calendars.CALENDAR_ACCESS_LEVEL + ">="
190            + Calendars.CAL_ACCESS_CONTRIBUTOR + " AND " + Calendars.VISIBLE + "=1";
191
192    static final String CALENDARS_WHERE = Calendars._ID + "=?";
193
194    static final String[] ATTENDEES_PROJECTION = new String[] {
195            Attendees._ID, // 0
196            Attendees.ATTENDEE_NAME, // 1
197            Attendees.ATTENDEE_EMAIL, // 2
198            Attendees.ATTENDEE_RELATIONSHIP, // 3
199            Attendees.ATTENDEE_STATUS, // 4
200    };
201    static final int ATTENDEES_INDEX_ID = 0;
202    static final int ATTENDEES_INDEX_NAME = 1;
203    static final int ATTENDEES_INDEX_EMAIL = 2;
204    static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
205    static final int ATTENDEES_INDEX_STATUS = 4;
206    static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=? AND attendeeEmail IS NOT NULL";
207
208    public static class AttendeeItem {
209        public boolean mRemoved;
210        public Attendee mAttendee;
211        public Drawable mBadge;
212        public int mUpdateCounts;
213        public View mView;
214        public Uri mContactLookupUri;
215
216        public AttendeeItem(Attendee attendee, Drawable badge) {
217            mAttendee = attendee;
218            mBadge = badge;
219        }
220    }
221
222    public EditEventHelper(Context context) {
223        mService = ((AbstractCalendarActivity)context).getAsyncQueryService();
224    }
225
226    public EditEventHelper(Context context, CalendarEventModel model) {
227        this(context);
228        // TODO: Remove unnecessary constructor.
229    }
230
231    /**
232     * Saves the event. Returns true if the event was successfully saved, false
233     * otherwise.
234     *
235     * @param model The event model to save
236     * @param originalModel A model of the original event if it exists
237     * @param modifyWhich For recurring events which type of series modification to use
238     * @return true if the event was successfully queued for saving
239     */
240    public boolean saveEvent(CalendarEventModel model, CalendarEventModel originalModel,
241            int modifyWhich) {
242        boolean forceSaveReminders = false;
243
244        if (DEBUG) {
245            Log.d(TAG, "Saving event model: " + model);
246        }
247
248        if (!mEventOk) {
249            if (DEBUG) {
250                Log.w(TAG, "Event no longer exists. Event was not saved.");
251            }
252            return false;
253        }
254
255        // It's a problem if we try to save a non-existent or invalid model or if we're
256        // modifying an existing event and we have the wrong original model
257        if (model == null) {
258            Log.e(TAG, "Attempted to save null model.");
259            return false;
260        }
261        if (!model.isValid()) {
262            Log.e(TAG, "Attempted to save invalid model.");
263            return false;
264        }
265        if (originalModel != null && !isSameEvent(model, originalModel)) {
266            Log.e(TAG, "Attempted to update existing event but models didn't refer to the same "
267                    + "event.");
268            return false;
269        }
270        if (originalModel != null && model.isUnchanged(originalModel)) {
271            return false;
272        }
273
274        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
275        int eventIdIndex = -1;
276
277        ContentValues values = getContentValuesFromModel(model);
278
279        if (model.mUri != null && originalModel == null) {
280            Log.e(TAG, "Existing event but no originalModel provided. Aborting save.");
281            return false;
282        }
283        Uri uri = null;
284        if (model.mUri != null) {
285            uri = Uri.parse(model.mUri);
286        }
287
288        // Update the "hasAlarm" field for the event
289        ArrayList<ReminderEntry> reminders = model.mReminders;
290        int len = reminders.size();
291        values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
292
293        if (uri == null) {
294            // Add hasAttendeeData for a new event
295            values.put(Events.HAS_ATTENDEE_DATA, 1);
296            values.put(Events.STATUS, Events.STATUS_CONFIRMED);
297            eventIdIndex = ops.size();
298            ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(
299                    Events.CONTENT_URI).withValues(values);
300            ops.add(b.build());
301            forceSaveReminders = true;
302
303        } else if (TextUtils.isEmpty(model.mRrule) && TextUtils.isEmpty(originalModel.mRrule)) {
304            // Simple update to a non-recurring event
305            checkTimeDependentFields(originalModel, model, values, modifyWhich);
306            ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
307
308        } else if (TextUtils.isEmpty(originalModel.mRrule)) {
309            // This event was changed from a non-repeating event to a
310            // repeating event.
311            ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
312
313        } else if (modifyWhich == MODIFY_SELECTED) {
314            // Modify contents of the current instance of repeating event
315            // Create a recurrence exception
316            long begin = model.mOriginalStart;
317            values.put(Events.ORIGINAL_SYNC_ID, originalModel.mSyncId);
318            values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
319            boolean allDay = originalModel.mAllDay;
320            values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
321            values.put(Events.STATUS, originalModel.mEventStatus);
322
323            eventIdIndex = ops.size();
324            ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(
325                    Events.CONTENT_URI).withValues(values);
326            ops.add(b.build());
327            forceSaveReminders = true;
328
329        } else if (modifyWhich == MODIFY_ALL_FOLLOWING) {
330
331            if (TextUtils.isEmpty(model.mRrule)) {
332                // We've changed a recurring event to a non-recurring event.
333                // If the event we are editing is the first in the series,
334                // then delete the whole series. Otherwise, update the series
335                // to end at the new start time.
336                if (isFirstEventInSeries(model, originalModel)) {
337                    ops.add(ContentProviderOperation.newDelete(uri).build());
338                } else {
339                    // Update the current repeating event to end at the new start time.  We
340                    // ignore the RRULE returned because the exception event doesn't want one.
341                    updatePastEvents(ops, originalModel, model.mOriginalStart);
342                }
343                eventIdIndex = ops.size();
344                values.put(Events.STATUS, originalModel.mEventStatus);
345                ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
346                        .build());
347            } else {
348                if (isFirstEventInSeries(model, originalModel)) {
349                    checkTimeDependentFields(originalModel, model, values, modifyWhich);
350                    ContentProviderOperation.Builder b = ContentProviderOperation.newUpdate(uri)
351                            .withValues(values);
352                    ops.add(b.build());
353                } else {
354                    // We need to update the existing recurrence to end before the exception
355                    // event starts.  If the recurrence rule has a COUNT, we need to adjust
356                    // that in the original and in the exception.  This call rewrites the
357                    // original event's recurrence rule (in "ops"), and returns a new rule
358                    // for the exception.  If the exception explicitly set a new rule, however,
359                    // we don't want to overwrite it.
360                    String newRrule = updatePastEvents(ops, originalModel, model.mOriginalStart);
361                    if (model.mRrule.equals(originalModel.mRrule)) {
362                        values.put(Events.RRULE, newRrule);
363                    }
364
365                    // Create a new event with the user-modified fields
366                    eventIdIndex = ops.size();
367                    values.put(Events.STATUS, originalModel.mEventStatus);
368                    ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
369                            values).build());
370                }
371            }
372            forceSaveReminders = true;
373
374        } else if (modifyWhich == MODIFY_ALL) {
375
376            // Modify all instances of repeating event
377            if (TextUtils.isEmpty(model.mRrule)) {
378                // We've changed a recurring event to a non-recurring event.
379                // Delete the whole series and replace it with a new
380                // non-recurring event.
381                ops.add(ContentProviderOperation.newDelete(uri).build());
382
383                eventIdIndex = ops.size();
384                ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
385                        .build());
386                forceSaveReminders = true;
387            } else {
388                checkTimeDependentFields(originalModel, model, values, modifyWhich);
389                ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
390            }
391        }
392
393        // New Event or New Exception to an existing event
394        boolean newEvent = (eventIdIndex != -1);
395        ArrayList<ReminderEntry> originalReminders;
396        if (originalModel != null) {
397            originalReminders = originalModel.mReminders;
398        } else {
399            originalReminders = new ArrayList<ReminderEntry>();
400        }
401
402        if (newEvent) {
403            saveRemindersWithBackRef(ops, eventIdIndex, reminders, originalReminders,
404                    forceSaveReminders);
405        } else if (uri != null) {
406            long eventId = ContentUris.parseId(uri);
407            saveReminders(ops, eventId, reminders, originalReminders, forceSaveReminders);
408        }
409
410        ContentProviderOperation.Builder b;
411        boolean hasAttendeeData = model.mHasAttendeeData;
412
413        if (hasAttendeeData && model.mOwnerAttendeeId == -1) {
414            // Organizer is not an attendee
415
416            String ownerEmail = model.mOwnerAccount;
417            if (model.mAttendeesList.size() != 0 && Utils.isValidEmail(ownerEmail)) {
418                // Add organizer as attendee since we got some attendees
419
420                values.clear();
421                values.put(Attendees.ATTENDEE_EMAIL, ownerEmail);
422                values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
423                values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
424                values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
425
426                if (newEvent) {
427                    b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
428                            .withValues(values);
429                    b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
430                } else {
431                    values.put(Attendees.EVENT_ID, model.mId);
432                    b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
433                            .withValues(values);
434                }
435                ops.add(b.build());
436            }
437        } else if (hasAttendeeData &&
438                model.mSelfAttendeeStatus != originalModel.mSelfAttendeeStatus &&
439                model.mOwnerAttendeeId != -1) {
440            if (DEBUG) {
441                Log.d(TAG, "Setting attendee status to " + model.mSelfAttendeeStatus);
442            }
443            Uri attUri = ContentUris.withAppendedId(Attendees.CONTENT_URI, model.mOwnerAttendeeId);
444
445            values.clear();
446            values.put(Attendees.ATTENDEE_STATUS, model.mSelfAttendeeStatus);
447            values.put(Attendees.EVENT_ID, model.mId);
448            b = ContentProviderOperation.newUpdate(attUri).withValues(values);
449            ops.add(b.build());
450        }
451
452        // TODO: is this the right test? this currently checks if this is
453        // a new event or an existing event. or is this a paranoia check?
454        if (hasAttendeeData && (newEvent || uri != null)) {
455            String attendees = model.getAttendeesString();
456            String originalAttendeesString;
457            if (originalModel != null) {
458                originalAttendeesString = originalModel.getAttendeesString();
459            } else {
460                originalAttendeesString = "";
461            }
462            // Hit the content provider only if this is a new event or the user
463            // has changed it
464            if (newEvent || !TextUtils.equals(originalAttendeesString, attendees)) {
465                // figure out which attendees need to be added and which ones
466                // need to be deleted. use a linked hash set, so we maintain
467                // order (but also remove duplicates).
468                HashMap<String, Attendee> newAttendees = model.mAttendeesList;
469                LinkedList<String> removedAttendees = new LinkedList<String>();
470
471                // the eventId is only used if eventIdIndex is -1.
472                // TODO: clean up this code.
473                long eventId = uri != null ? ContentUris.parseId(uri) : -1;
474
475                // only compute deltas if this is an existing event.
476                // new events (being inserted into the Events table) won't
477                // have any existing attendees.
478                if (!newEvent) {
479                    removedAttendees.clear();
480                    HashMap<String, Attendee> originalAttendees = originalModel.mAttendeesList;
481                    for (String originalEmail : originalAttendees.keySet()) {
482                        if (newAttendees.containsKey(originalEmail)) {
483                            // existing attendee. remove from new attendees set.
484                            newAttendees.remove(originalEmail);
485                        } else {
486                            // no longer in attendees. mark as removed.
487                            removedAttendees.add(originalEmail);
488                        }
489                    }
490
491                    // delete removed attendees if necessary
492                    if (removedAttendees.size() > 0) {
493                        b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
494
495                        String[] args = new String[removedAttendees.size() + 1];
496                        args[0] = Long.toString(eventId);
497                        int i = 1;
498                        StringBuilder deleteWhere = new StringBuilder(ATTENDEES_DELETE_PREFIX);
499                        for (String removedAttendee : removedAttendees) {
500                            if (i > 1) {
501                                deleteWhere.append(",");
502                            }
503                            deleteWhere.append("?");
504                            args[i++] = removedAttendee;
505                        }
506                        deleteWhere.append(")");
507                        b.withSelection(deleteWhere.toString(), args);
508                        ops.add(b.build());
509                    }
510                }
511
512                if (newAttendees.size() > 0) {
513                    // Insert the new attendees
514                    for (Attendee attendee : newAttendees.values()) {
515                        values.clear();
516                        values.put(Attendees.ATTENDEE_NAME, attendee.mName);
517                        values.put(Attendees.ATTENDEE_EMAIL, attendee.mEmail);
518                        values.put(Attendees.ATTENDEE_RELATIONSHIP,
519                                Attendees.RELATIONSHIP_ATTENDEE);
520                        values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
521                        values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
522
523                        if (newEvent) {
524                            b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
525                                    .withValues(values);
526                            b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
527                        } else {
528                            values.put(Attendees.EVENT_ID, eventId);
529                            b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
530                                    .withValues(values);
531                        }
532                        ops.add(b.build());
533                    }
534                }
535            }
536        }
537
538
539        mService.startBatch(mService.getNextToken(), null, android.provider.CalendarContract.AUTHORITY, ops,
540                Utils.UNDO_DELAY);
541
542        return true;
543    }
544
545    public static LinkedHashSet<Rfc822Token> getAddressesFromList(String list,
546            Rfc822Validator validator) {
547        LinkedHashSet<Rfc822Token> addresses = new LinkedHashSet<Rfc822Token>();
548        Rfc822Tokenizer.tokenize(list, addresses);
549        if (validator == null) {
550            return addresses;
551        }
552
553        // validate the emails, out of paranoia. they should already be
554        // validated on input, but drop any invalid emails just to be safe.
555        Iterator<Rfc822Token> addressIterator = addresses.iterator();
556        while (addressIterator.hasNext()) {
557            Rfc822Token address = addressIterator.next();
558            if (!validator.isValid(address.getAddress())) {
559                Log.v(TAG, "Dropping invalid attendee email address: " + address.getAddress());
560                addressIterator.remove();
561            }
562        }
563        return addresses;
564    }
565
566    /**
567     * When we aren't given an explicit start time, we default to the next
568     * upcoming half hour. So, for example, 5:01 -> 5:30, 5:30 -> 6:00, etc.
569     *
570     * @return a UTC time in milliseconds representing the next upcoming half
571     * hour
572     */
573    protected long constructDefaultStartTime(long now) {
574        Time defaultStart = new Time();
575        defaultStart.set(now);
576        defaultStart.second = 0;
577        defaultStart.minute = 30;
578        long defaultStartMillis = defaultStart.toMillis(false);
579        if (now < defaultStartMillis) {
580            return defaultStartMillis;
581        } else {
582            return defaultStartMillis + 30 * DateUtils.MINUTE_IN_MILLIS;
583        }
584    }
585
586    /**
587     * When we aren't given an explicit end time, we default to an hour after
588     * the start time.
589     * @param startTime the start time
590     * @return a default end time
591     */
592    protected long constructDefaultEndTime(long startTime) {
593        return startTime + DateUtils.HOUR_IN_MILLIS;
594    }
595
596    // TODO think about how useful this is. Probably check if our event has
597    // changed early on and either update all or nothing. Should still do the if
598    // MODIFY_ALL bit.
599    void checkTimeDependentFields(CalendarEventModel originalModel, CalendarEventModel model,
600            ContentValues values, int modifyWhich) {
601        long oldBegin = model.mOriginalStart;
602        long oldEnd = model.mOriginalEnd;
603        boolean oldAllDay = originalModel.mAllDay;
604        String oldRrule = originalModel.mRrule;
605        String oldTimezone = originalModel.mTimezone;
606
607        long newBegin = model.mStart;
608        long newEnd = model.mEnd;
609        boolean newAllDay = model.mAllDay;
610        String newRrule = model.mRrule;
611        String newTimezone = model.mTimezone;
612
613        // If none of the time-dependent fields changed, then remove them.
614        if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
615                && TextUtils.equals(oldRrule, newRrule)
616                && TextUtils.equals(oldTimezone, newTimezone)) {
617            values.remove(Events.DTSTART);
618            values.remove(Events.DTEND);
619            values.remove(Events.DURATION);
620            values.remove(Events.ALL_DAY);
621            values.remove(Events.RRULE);
622            values.remove(Events.EVENT_TIMEZONE);
623            return;
624        }
625
626        if (TextUtils.isEmpty(oldRrule) || TextUtils.isEmpty(newRrule)) {
627            return;
628        }
629
630        // If we are modifying all events then we need to set DTSTART to the
631        // start time of the first event in the series, not the current
632        // date and time. If the start time of the event was changed
633        // (from, say, 3pm to 4pm), then we want to add the time difference
634        // to the start time of the first event in the series (the DTSTART
635        // value). If we are modifying one instance or all following instances,
636        // then we leave the DTSTART field alone.
637        if (modifyWhich == MODIFY_ALL) {
638            long oldStartMillis = originalModel.mStart;
639            if (oldBegin != newBegin) {
640                // The user changed the start time of this event
641                long offset = newBegin - oldBegin;
642                oldStartMillis += offset;
643            }
644            if (newAllDay) {
645                Time time = new Time(Time.TIMEZONE_UTC);
646                time.set(oldStartMillis);
647                time.hour = 0;
648                time.minute = 0;
649                time.second = 0;
650                oldStartMillis = time.toMillis(false);
651            }
652            values.put(Events.DTSTART, oldStartMillis);
653        }
654    }
655
656    /**
657     * Prepares an update to the original event so it stops where the new series
658     * begins. When we update 'this and all following' events we need to change
659     * the original event to end before a new series starts. This creates an
660     * update to the old event's rrule to do that.
661     *<p>
662     * If the event's recurrence rule has a COUNT, we also need to reduce the count in the
663     * RRULE for the exception event.
664     *
665     * @param ops The list of operations to add the update to
666     * @param originalModel The original event that we're updating
667     * @param endTimeMillis The time before which the event must end (i.e. the start time of the
668     *        exception event instance).
669     * @return A replacement exception recurrence rule.
670     */
671    public String updatePastEvents(ArrayList<ContentProviderOperation> ops,
672            CalendarEventModel originalModel, long endTimeMillis) {
673        boolean origAllDay = originalModel.mAllDay;
674        String origRrule = originalModel.mRrule;
675        String newRrule = origRrule;
676
677        EventRecurrence origRecurrence = new EventRecurrence();
678        origRecurrence.parse(origRrule);
679
680        // Get the start time of the first instance in the original recurrence.
681        long startTimeMillis = originalModel.mStart;
682        Time dtstart = new Time();
683        dtstart.timezone = originalModel.mTimezone;
684        dtstart.set(startTimeMillis);
685
686        ContentValues updateValues = new ContentValues();
687
688        if (origRecurrence.count > 0) {
689            /*
690             * Generate the full set of instances for this recurrence, from the first to the
691             * one just before endTimeMillis.  The list should never be empty, because this method
692             * should not be called for the first instance.  All we're really interested in is
693             * the *number* of instances found.
694             *
695             * TODO: the model assumes RRULE and ignores RDATE, EXRULE, and EXDATE.  For the
696             * current environment this is reasonable, but that may not hold in the future.
697             *
698             * TODO: if COUNT is 1, should we convert the event to non-recurring?  e.g. we
699             * do an "edit this and all future events" on the 2nd instances.
700             */
701            RecurrenceSet recurSet = new RecurrenceSet(originalModel.mRrule, null, null, null);
702            RecurrenceProcessor recurProc = new RecurrenceProcessor();
703            long[] recurrences;
704            try {
705                recurrences = recurProc.expand(dtstart, recurSet, startTimeMillis, endTimeMillis);
706            } catch (DateException de) {
707                throw new RuntimeException(de);
708            }
709
710            if (recurrences.length == 0) {
711                throw new RuntimeException("can't use this method on first instance");
712            }
713
714            EventRecurrence excepRecurrence = new EventRecurrence();
715            excepRecurrence.parse(origRrule);  // TODO: add+use a copy constructor instead
716            excepRecurrence.count -= recurrences.length;
717            newRrule = excepRecurrence.toString();
718
719            origRecurrence.count = recurrences.length;
720
721        } else {
722            // The "until" time must be in UTC time in order for Google calendar
723            // to display it properly. For all-day events, the "until" time string
724            // must include just the date field, and not the time field. The
725            // repeating events repeat up to and including the "until" time.
726            Time untilTime = new Time();
727            untilTime.timezone = Time.TIMEZONE_UTC;
728
729            // Subtract one second from the old begin time to get the new
730            // "until" time.
731            untilTime.set(endTimeMillis - 1000); // subtract one second (1000 millis)
732            if (origAllDay) {
733                untilTime.hour = 0;
734                untilTime.minute = 0;
735                untilTime.second = 0;
736                untilTime.allDay = true;
737                untilTime.normalize(false);
738
739                // This should no longer be necessary -- DTSTART should already be in the correct
740                // format for an all-day event.
741                dtstart.hour = 0;
742                dtstart.minute = 0;
743                dtstart.second = 0;
744                dtstart.allDay = true;
745                dtstart.timezone = Time.TIMEZONE_UTC;
746            }
747            origRecurrence.until = untilTime.format2445();
748        }
749
750        updateValues.put(Events.RRULE, origRecurrence.toString());
751        updateValues.put(Events.DTSTART, dtstart.normalize(true));
752        ContentProviderOperation.Builder b =
753                ContentProviderOperation.newUpdate(Uri.parse(originalModel.mUri))
754                .withValues(updateValues);
755        ops.add(b.build());
756
757        return newRrule;
758    }
759
760    /**
761     * Compares two models to ensure that they refer to the same event. This is
762     * a safety check to make sure an updated event model refers to the same
763     * event as the original model. If the original model is null then this is a
764     * new event or we're forcing an overwrite so we return true in that case.
765     * The important identifiers are the Calendar Id and the Event Id.
766     *
767     * @return
768     */
769    public static boolean isSameEvent(CalendarEventModel model, CalendarEventModel originalModel) {
770        if (originalModel == null) {
771            return true;
772        }
773
774        if (model.mCalendarId != originalModel.mCalendarId) {
775            return false;
776        }
777        if (model.mId != originalModel.mId) {
778            return false;
779        }
780
781        return true;
782    }
783
784    /**
785     * Saves the reminders, if they changed. Returns true if operations to
786     * update the database were added.
787     *
788     * @param ops the array of ContentProviderOperations
789     * @param eventId the id of the event whose reminders are being updated
790     * @param reminders the array of reminders set by the user
791     * @param originalReminders the original array of reminders
792     * @param forceSave if true, then save the reminders even if they didn't change
793     * @return true if operations to update the database were added
794     */
795    public static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
796            ArrayList<ReminderEntry> reminders, ArrayList<ReminderEntry> originalReminders,
797            boolean forceSave) {
798        // If the reminders have not changed, then don't update the database
799        if (reminders.equals(originalReminders) && !forceSave) {
800            return false;
801        }
802
803        // Delete all the existing reminders for this event
804        String where = Reminders.EVENT_ID + "=?";
805        String[] args = new String[] {Long.toString(eventId)};
806        ContentProviderOperation.Builder b = ContentProviderOperation
807                .newDelete(Reminders.CONTENT_URI);
808        b.withSelection(where, args);
809        ops.add(b.build());
810
811        ContentValues values = new ContentValues();
812        int len = reminders.size();
813
814        // Insert the new reminders, if any
815        for (int i = 0; i < len; i++) {
816            ReminderEntry re = reminders.get(i);
817
818            values.clear();
819            values.put(Reminders.MINUTES, re.getMinutes());
820            values.put(Reminders.METHOD, re.getMethod());
821            values.put(Reminders.EVENT_ID, eventId);
822            b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
823            ops.add(b.build());
824        }
825        return true;
826    }
827
828    /**
829     * Saves the reminders, if they changed. Returns true if operations to
830     * update the database were added. Uses a reference id since an id isn't
831     * created until the row is added.
832     *
833     * @param ops the array of ContentProviderOperations
834     * @param eventId the id of the event whose reminders are being updated
835     * @param reminderMinutes the array of reminders set by the user
836     * @param originalMinutes the original array of reminders
837     * @param forceSave if true, then save the reminders even if they didn't change
838     * @return true if operations to update the database were added
839     */
840    public static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
841            int eventIdIndex, ArrayList<ReminderEntry> reminders,
842            ArrayList<ReminderEntry> originalReminders, boolean forceSave) {
843        // If the reminders have not changed, then don't update the database
844        if (reminders.equals(originalReminders) && !forceSave) {
845            return false;
846        }
847
848        // Delete all the existing reminders for this event
849        ContentProviderOperation.Builder b = ContentProviderOperation
850                .newDelete(Reminders.CONTENT_URI);
851        b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
852        b.withSelectionBackReference(0, eventIdIndex);
853        ops.add(b.build());
854
855        ContentValues values = new ContentValues();
856        int len = reminders.size();
857
858        // Insert the new reminders, if any
859        for (int i = 0; i < len; i++) {
860            ReminderEntry re = reminders.get(i);
861
862            values.clear();
863            values.put(Reminders.MINUTES, re.getMinutes());
864            values.put(Reminders.METHOD, re.getMethod());
865            b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
866            b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
867            ops.add(b.build());
868        }
869        return true;
870    }
871
872    // It's the first event in the series if the start time before being
873    // modified is the same as the original event's start time
874    static boolean isFirstEventInSeries(CalendarEventModel model,
875            CalendarEventModel originalModel) {
876        return model.mOriginalStart == originalModel.mStart;
877    }
878
879    // Adds an rRule and duration to a set of content values
880    void addRecurrenceRule(ContentValues values, CalendarEventModel model) {
881        String rrule = model.mRrule;
882
883        values.put(Events.RRULE, rrule);
884        long end = model.mEnd;
885        long start = model.mStart;
886        String duration = model.mDuration;
887
888        boolean isAllDay = model.mAllDay;
889        if (end > start) {
890            if (isAllDay) {
891                // if it's all day compute the duration in days
892                long days = (end - start + DateUtils.DAY_IN_MILLIS - 1)
893                        / DateUtils.DAY_IN_MILLIS;
894                duration = "P" + days + "D";
895            } else {
896                // otherwise compute the duration in seconds
897                long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
898                duration = "P" + seconds + "S";
899            }
900        } else if (TextUtils.isEmpty(duration)) {
901
902            // If no good duration info exists assume the default
903            if (isAllDay) {
904                duration = "P1D";
905            } else {
906                duration = "P3600S";
907            }
908        }
909        // recurring events should have a duration and dtend set to null
910        values.put(Events.DURATION, duration);
911        values.put(Events.DTEND, (Long) null);
912    }
913
914    /**
915     * Uses the recurrence selection and the model data to build an rrule and
916     * write it to the model.
917     *
918     * @param selection the type of rrule
919     * @param model The event to update
920     * @param weekStart the week start day, specified as java.util.Calendar
921     * constants
922     */
923    static void updateRecurrenceRule(int selection, CalendarEventModel model,
924            int weekStart) {
925        // Make sure we don't have any leftover data from the previous setting
926        EventRecurrence eventRecurrence = new EventRecurrence();
927
928        if (selection == DOES_NOT_REPEAT) {
929            model.mRrule = null;
930            return;
931        } else if (selection == REPEATS_CUSTOM) {
932            // Keep custom recurrence as before.
933            return;
934        } else if (selection == REPEATS_DAILY) {
935            eventRecurrence.freq = EventRecurrence.DAILY;
936        } else if (selection == REPEATS_EVERY_WEEKDAY) {
937            eventRecurrence.freq = EventRecurrence.WEEKLY;
938            int dayCount = 5;
939            int[] byday = new int[dayCount];
940            int[] bydayNum = new int[dayCount];
941
942            byday[0] = EventRecurrence.MO;
943            byday[1] = EventRecurrence.TU;
944            byday[2] = EventRecurrence.WE;
945            byday[3] = EventRecurrence.TH;
946            byday[4] = EventRecurrence.FR;
947            for (int day = 0; day < dayCount; day++) {
948                bydayNum[day] = 0;
949            }
950
951            eventRecurrence.byday = byday;
952            eventRecurrence.bydayNum = bydayNum;
953            eventRecurrence.bydayCount = dayCount;
954        } else if (selection == REPEATS_WEEKLY_ON_DAY) {
955            eventRecurrence.freq = EventRecurrence.WEEKLY;
956            int[] days = new int[1];
957            int dayCount = 1;
958            int[] dayNum = new int[dayCount];
959            Time startTime = new Time(model.mTimezone);
960            startTime.set(model.mStart);
961
962            days[0] = EventRecurrence.timeDay2Day(startTime.weekDay);
963            // not sure why this needs to be zero, but set it for now.
964            dayNum[0] = 0;
965
966            eventRecurrence.byday = days;
967            eventRecurrence.bydayNum = dayNum;
968            eventRecurrence.bydayCount = dayCount;
969        } else if (selection == REPEATS_MONTHLY_ON_DAY) {
970            eventRecurrence.freq = EventRecurrence.MONTHLY;
971            eventRecurrence.bydayCount = 0;
972            eventRecurrence.bymonthdayCount = 1;
973            int[] bymonthday = new int[1];
974            Time startTime = new Time(model.mTimezone);
975            startTime.set(model.mStart);
976            bymonthday[0] = startTime.monthDay;
977            eventRecurrence.bymonthday = bymonthday;
978        } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
979            eventRecurrence.freq = EventRecurrence.MONTHLY;
980            eventRecurrence.bydayCount = 1;
981            eventRecurrence.bymonthdayCount = 0;
982
983            int[] byday = new int[1];
984            int[] bydayNum = new int[1];
985            Time startTime = new Time(model.mTimezone);
986            startTime.set(model.mStart);
987            // Compute the week number (for example, the "2nd" Monday)
988            int dayCount = 1 + ((startTime.monthDay - 1) / 7);
989            if (dayCount == 5) {
990                dayCount = -1;
991            }
992            bydayNum[0] = dayCount;
993            byday[0] = EventRecurrence.timeDay2Day(startTime.weekDay);
994            eventRecurrence.byday = byday;
995            eventRecurrence.bydayNum = bydayNum;
996        } else if (selection == REPEATS_YEARLY) {
997            eventRecurrence.freq = EventRecurrence.YEARLY;
998        }
999
1000        // Set the week start day.
1001        eventRecurrence.wkst = EventRecurrence.calendarDay2Day(weekStart);
1002        model.mRrule = eventRecurrence.toString();
1003    }
1004
1005    /**
1006     * Uses an event cursor to fill in the given model This method assumes the
1007     * cursor used {@link #EVENT_PROJECTION} as it's query projection. It uses
1008     * the cursor to fill in the given model with all the information available.
1009     *
1010     * @param model The model to fill in
1011     * @param cursor An event cursor that used {@link #EVENT_PROJECTION} for the query
1012     */
1013    public static void setModelFromCursor(CalendarEventModel model, Cursor cursor) {
1014        if (model == null || cursor == null || cursor.getCount() != 1) {
1015            Log.wtf(TAG, "Attempted to build non-existent model or from an incorrect query.");
1016            return;
1017        }
1018
1019        model.clear();
1020        cursor.moveToFirst();
1021
1022        model.mId = cursor.getInt(EVENT_INDEX_ID);
1023        model.mTitle = cursor.getString(EVENT_INDEX_TITLE);
1024        model.mDescription = cursor.getString(EVENT_INDEX_DESCRIPTION);
1025        model.mLocation = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
1026        model.mAllDay = cursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1027        model.mHasAlarm = cursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
1028        model.mCalendarId = cursor.getInt(EVENT_INDEX_CALENDAR_ID);
1029        model.mStart = cursor.getLong(EVENT_INDEX_DTSTART);
1030        String tz = cursor.getString(EVENT_INDEX_TIMEZONE);
1031        if (!TextUtils.isEmpty(tz)) {
1032            model.mTimezone = tz;
1033        }
1034        String rRule = cursor.getString(EVENT_INDEX_RRULE);
1035        model.mRrule = rRule;
1036        model.mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
1037        model.mAvailability = cursor.getInt(EVENT_INDEX_AVAILABILITY);
1038        int accessLevel = cursor.getInt(EVENT_INDEX_ACCESS_LEVEL);
1039        model.mOwnerAccount = cursor.getString(EVENT_INDEX_OWNER_ACCOUNT);
1040        model.mHasAttendeeData = cursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
1041        model.mOriginalSyncId = cursor.getString(EVENT_INDEX_ORIGINAL_SYNC_ID);
1042        model.mOriginalId = cursor.getLong(EVENT_INDEX_ORIGINAL_ID);
1043        model.mOrganizer = cursor.getString(EVENT_INDEX_ORGANIZER);
1044        model.mIsOrganizer = model.mOwnerAccount.equalsIgnoreCase(model.mOrganizer);
1045        model.mGuestsCanModify = cursor.getInt(EVENT_INDEX_GUESTS_CAN_MODIFY) != 0;
1046
1047        if (accessLevel > 0) {
1048            // For now the array contains the values 0, 2, and 3. We subtract
1049            // one to make it easier to handle in code as 0,1,2.
1050            // Default (0), Private (1), Public (2)
1051            accessLevel--;
1052        }
1053        model.mAccessLevel = accessLevel;
1054        model.mEventStatus = cursor.getInt(EVENT_INDEX_EVENT_STATUS);
1055
1056        boolean hasRRule = !TextUtils.isEmpty(rRule);
1057
1058        // We expect only one of these, so ignore the other
1059        if (hasRRule) {
1060            model.mDuration = cursor.getString(EVENT_INDEX_DURATION);
1061        } else {
1062            model.mEnd = cursor.getLong(EVENT_INDEX_DTEND);
1063        }
1064
1065        model.mModelUpdatedWithEventCursor = true;
1066    }
1067
1068    /**
1069     * Uses a calendar cursor to fill in the given model This method assumes the
1070     * cursor used {@link #CALENDARS_PROJECTION} as it's query projection. It uses
1071     * the cursor to fill in the given model with all the information available.
1072     *
1073     * @param model The model to fill in
1074     * @param cursor An event cursor that used {@link #CALENDARS_PROJECTION} for the query
1075     * @return returns true if model was updated with the info in the cursor.
1076     */
1077    public static boolean setModelFromCalendarCursor(CalendarEventModel model, Cursor cursor) {
1078        if (model == null || cursor == null) {
1079            Log.wtf(TAG, "Attempted to build non-existent model or from an incorrect query.");
1080            return false;
1081        }
1082
1083        if (model.mCalendarId == -1) {
1084            return false;
1085        }
1086
1087        if (!model.mModelUpdatedWithEventCursor) {
1088            Log.wtf(TAG,
1089                    "Can't update model with a Calendar cursor until it has seen an Event cursor.");
1090            return false;
1091        }
1092
1093        cursor.moveToPosition(-1);
1094        while (cursor.moveToNext()) {
1095            if (model.mCalendarId != cursor.getInt(CALENDARS_INDEX_ID)) {
1096                continue;
1097            }
1098
1099            model.mOrganizerCanRespond = cursor.getInt(CALENDARS_INDEX_CAN_ORGANIZER_RESPOND) != 0;
1100
1101            model.mCalendarAccessLevel = cursor.getInt(CALENDARS_INDEX_ACCESS_LEVEL);
1102            model.mCalendarDisplayName = cursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
1103            model.mCalendarColor = cursor.getInt(CALENDARS_INDEX_COLOR);
1104
1105            model.mCalendarMaxReminders = cursor.getInt(CALENDARS_INDEX_MAX_REMINDERS);
1106            model.mCalendarAllowedReminders = cursor.getString(CALENDARS_INDEX_ALLOWED_REMINDERS);
1107            model.mCalendarAllowedAttendeeTypes = cursor
1108                    .getString(CALENDARS_INDEX_ALLOWED_ATTENDEE_TYPES);
1109            model.mCalendarAllowedAvailability = cursor
1110                    .getString(CALENDARS_INDEX_ALLOWED_AVAILABILITY);
1111
1112            return true;
1113       }
1114       return false;
1115    }
1116
1117    public static boolean canModifyEvent(CalendarEventModel model) {
1118        return canModifyCalendar(model)
1119                && (model.mIsOrganizer || model.mGuestsCanModify);
1120    }
1121
1122    public static boolean canModifyCalendar(CalendarEventModel model) {
1123        return model.mCalendarAccessLevel >= Calendars.CAL_ACCESS_CONTRIBUTOR
1124                || model.mCalendarId == -1;
1125    }
1126
1127    public static boolean canAddReminders(CalendarEventModel model) {
1128        return model.mCalendarAccessLevel >= Calendars.CAL_ACCESS_READ;
1129    }
1130
1131    public static boolean canRespond(CalendarEventModel model) {
1132        // For non-organizers, write permission to the calendar is sufficient.
1133        // For organizers, the user needs a) write permission to the calendar
1134        // AND b) ownerCanRespond == true AND c) attendee data exist
1135        // (this means num of attendees > 1, the calendar owner's and others).
1136        // Note that mAttendeeList omits the organizer.
1137
1138        // (there are more cases involved to be 100% accurate, such as
1139        // paying attention to whether or not an attendee status was
1140        // included in the feed, but we're currently omitting those corner cases
1141        // for simplicity).
1142
1143        if (!canModifyCalendar(model)) {
1144            return false;
1145        }
1146
1147        if (!model.mIsOrganizer) {
1148            return true;
1149        }
1150
1151        if (!model.mOrganizerCanRespond) {
1152            return false;
1153        }
1154
1155        // This means we don't have the attendees data so we can't send
1156        // the list of attendees and the status back to the server
1157        if (model.mHasAttendeeData && model.mAttendeesList.size() == 0) {
1158            return false;
1159        }
1160
1161        return true;
1162    }
1163
1164    /**
1165     * Goes through an event model and fills in content values for saving. This
1166     * method will perform the initial collection of values from the model and
1167     * put them into a set of ContentValues. It performs some basic work such as
1168     * fixing the time on allDay events and choosing whether to use an rrule or
1169     * dtend.
1170     *
1171     * @param model The complete model of the event you want to save
1172     * @return values
1173     */
1174    ContentValues getContentValuesFromModel(CalendarEventModel model) {
1175        String title = model.mTitle;
1176        boolean isAllDay = model.mAllDay;
1177        String rrule = model.mRrule;
1178        String timezone = model.mTimezone;
1179        if (timezone == null) {
1180            timezone = TimeZone.getDefault().getID();
1181        }
1182        Time startTime = new Time(timezone);
1183        Time endTime = new Time(timezone);
1184
1185        startTime.set(model.mStart);
1186        endTime.set(model.mEnd);
1187
1188        ContentValues values = new ContentValues();
1189
1190        long startMillis;
1191        long endMillis;
1192        long calendarId = model.mCalendarId;
1193        if (isAllDay) {
1194            // Reset start and end time, ensure at least 1 day duration, and set
1195            // the timezone to UTC, as required for all-day events.
1196            timezone = Time.TIMEZONE_UTC;
1197            startTime.hour = 0;
1198            startTime.minute = 0;
1199            startTime.second = 0;
1200            startTime.timezone = timezone;
1201            startMillis = startTime.normalize(true);
1202
1203            endTime.hour = 0;
1204            endTime.minute = 0;
1205            endTime.second = 0;
1206            endTime.timezone = timezone;
1207            endMillis = endTime.normalize(true);
1208            if (endMillis < startMillis + DateUtils.DAY_IN_MILLIS) {
1209                // EditEventView#fillModelFromUI() should treat this case, but we want to ensure
1210                // the condition anyway.
1211                endMillis = startMillis + DateUtils.DAY_IN_MILLIS;
1212            }
1213        } else {
1214            startMillis = startTime.toMillis(true);
1215            endMillis = endTime.toMillis(true);
1216        }
1217
1218        values.put(Events.CALENDAR_ID, calendarId);
1219        values.put(Events.EVENT_TIMEZONE, timezone);
1220        values.put(Events.TITLE, title);
1221        values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
1222        values.put(Events.DTSTART, startMillis);
1223        values.put(Events.RRULE, rrule);
1224        if (!TextUtils.isEmpty(rrule)) {
1225            addRecurrenceRule(values, model);
1226        } else {
1227            values.put(Events.DURATION, (String) null);
1228            values.put(Events.DTEND, endMillis);
1229        }
1230        if (model.mDescription != null) {
1231            values.put(Events.DESCRIPTION, model.mDescription.trim());
1232        } else {
1233            values.put(Events.DESCRIPTION, (String) null);
1234        }
1235        if (model.mLocation != null) {
1236            values.put(Events.EVENT_LOCATION, model.mLocation.trim());
1237        } else {
1238            values.put(Events.EVENT_LOCATION, (String) null);
1239        }
1240        values.put(Events.AVAILABILITY, model.mAvailability);
1241        values.put(Events.HAS_ATTENDEE_DATA, model.mHasAttendeeData ? 1 : 0);
1242
1243        int accessLevel = model.mAccessLevel;
1244        if (accessLevel > 0) {
1245            // For now the array contains the values 0, 2, and 3. We add one to match.
1246            // Default (0), Private (2), Public (3)
1247            accessLevel++;
1248        }
1249        values.put(Events.ACCESS_LEVEL, accessLevel);
1250        values.put(Events.STATUS, model.mEventStatus);
1251
1252        return values;
1253    }
1254
1255    /**
1256     * Takes an e-mail address and returns the domain (everything after the last @)
1257     */
1258    public static String extractDomain(String email) {
1259        int separator = email.lastIndexOf('@');
1260        if (separator != -1 && ++separator < email.length()) {
1261            return email.substring(separator);
1262        }
1263        return null;
1264    }
1265
1266    public interface EditDoneRunnable extends Runnable {
1267        public void setDoneCode(int code);
1268    }
1269}
1270