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