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