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