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