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 
AttendeeItem(Attendee attendee, Drawable badge)244         public AttendeeItem(Attendee attendee, Drawable badge) {
245             mAttendee = attendee;
246             mBadge = badge;
247         }
248     }
249 
EditEventHelper(Context context)250     public EditEventHelper(Context context) {
251         mService = ((AbstractCalendarActivity)context).getAsyncQueryService();
252     }
253 
EditEventHelper(Context context, CalendarEventModel model)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      */
saveEvent(CalendarEventModel model, CalendarEventModel originalModel, int modifyWhich)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 
getAddressesFromList(String list, Rfc822Validator validator)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      */
constructDefaultStartTime(long now)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      */
constructDefaultEndTime(long startTime)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.
checkTimeDependentFields(CalendarEventModel originalModel, CalendarEventModel model, ContentValues values, int modifyWhich)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      */
updatePastEvents(ArrayList<ContentProviderOperation> ops, CalendarEventModel originalModel, long endTimeMillis)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      */
isSameEvent(CalendarEventModel model, CalendarEventModel originalModel)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      */
saveReminders(ArrayList<ContentProviderOperation> ops, long eventId, ArrayList<ReminderEntry> reminders, ArrayList<ReminderEntry> originalReminders, boolean forceSave)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      */
saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops, int eventIdIndex, ArrayList<ReminderEntry> reminders, ArrayList<ReminderEntry> originalReminders, boolean forceSave)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
isFirstEventInSeries(CalendarEventModel model, CalendarEventModel originalModel)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
addRecurrenceRule(ContentValues values, CalendarEventModel model)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      */
updateRecurrenceRule(int selection, CalendarEventModel model, int weekStart)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      */
setModelFromCursor(CalendarEventModel model, Cursor cursor)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      */
setModelFromCalendarCursor(CalendarEventModel model, Cursor cursor)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 
canModifyEvent(CalendarEventModel model)1157     public static boolean canModifyEvent(CalendarEventModel model) {
1158         return canModifyCalendar(model)
1159                 && (model.mIsOrganizer || model.mGuestsCanModify);
1160     }
1161 
canModifyCalendar(CalendarEventModel model)1162     public static boolean canModifyCalendar(CalendarEventModel model) {
1163         return model.mCalendarAccessLevel >= Calendars.CAL_ACCESS_CONTRIBUTOR
1164                 || model.mCalendarId == -1;
1165     }
1166 
canAddReminders(CalendarEventModel model)1167     public static boolean canAddReminders(CalendarEventModel model) {
1168         return model.mCalendarAccessLevel >= Calendars.CAL_ACCESS_READ;
1169     }
1170 
canRespond(CalendarEventModel model)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      */
getContentValuesFromModel(CalendarEventModel model)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      */
offsetStartTimeIfNecessary(Time startTime, Time endTime, String rrule, CalendarEventModel model)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      */
extractDomain(String email)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 {
setDoneCode(int code)1388         public void setDoneCode(int code);
1389     }
1390 }
1391