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