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