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