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