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