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