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