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