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