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