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