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