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