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