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