1 /* 2 * Copyright (C) 2008 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; 18 19 import static android.provider.Calendar.EVENT_BEGIN_TIME; 20 import static android.provider.Calendar.EVENT_END_TIME; 21 22 import com.android.common.Rfc822InputFilter; 23 import com.android.common.Rfc822Validator; 24 25 import android.app.Activity; 26 import android.app.AlertDialog; 27 import android.app.DatePickerDialog; 28 import android.app.ProgressDialog; 29 import android.app.TimePickerDialog; 30 import android.app.DatePickerDialog.OnDateSetListener; 31 import android.app.TimePickerDialog.OnTimeSetListener; 32 import android.content.AsyncQueryHandler; 33 import android.content.ContentProviderOperation; 34 import android.content.ContentProviderResult; 35 import android.content.ContentResolver; 36 import android.content.ContentUris; 37 import android.content.ContentValues; 38 import android.content.Context; 39 import android.content.DialogInterface; 40 import android.content.Intent; 41 import android.content.OperationApplicationException; 42 import android.content.SharedPreferences; 43 import android.content.ContentProviderOperation.Builder; 44 import android.content.DialogInterface.OnCancelListener; 45 import android.content.DialogInterface.OnClickListener; 46 import android.content.res.Resources; 47 import android.database.Cursor; 48 import android.net.Uri; 49 import android.os.Bundle; 50 import android.os.RemoteException; 51 import android.pim.EventRecurrence; 52 import android.provider.Calendar.Attendees; 53 import android.provider.Calendar.Calendars; 54 import android.provider.Calendar.Events; 55 import android.provider.Calendar.Reminders; 56 import android.text.Editable; 57 import android.text.InputFilter; 58 import android.text.TextUtils; 59 import android.text.format.DateFormat; 60 import android.text.format.DateUtils; 61 import android.text.format.Time; 62 import android.text.util.Rfc822Token; 63 import android.text.util.Rfc822Tokenizer; 64 import android.util.Log; 65 import android.view.LayoutInflater; 66 import android.view.Menu; 67 import android.view.MenuItem; 68 import android.view.View; 69 import android.view.Window; 70 import android.widget.AdapterView; 71 import android.widget.ArrayAdapter; 72 import android.widget.Button; 73 import android.widget.CheckBox; 74 import android.widget.CompoundButton; 75 import android.widget.DatePicker; 76 import android.widget.ImageButton; 77 import android.widget.LinearLayout; 78 import android.widget.MultiAutoCompleteTextView; 79 import android.widget.ResourceCursorAdapter; 80 import android.widget.Spinner; 81 import android.widget.TextView; 82 import android.widget.TimePicker; 83 import android.widget.Toast; 84 85 import java.util.ArrayList; 86 import java.util.Arrays; 87 import java.util.Calendar; 88 import java.util.HashMap; 89 import java.util.HashSet; 90 import java.util.Iterator; 91 import java.util.LinkedHashSet; 92 import java.util.TimeZone; 93 94 public class EditEvent extends Activity implements View.OnClickListener, 95 DialogInterface.OnCancelListener, DialogInterface.OnClickListener { 96 private static final String TAG = "EditEvent"; 97 private static final boolean DEBUG = false; 98 99 /** 100 * This is the symbolic name for the key used to pass in the boolean 101 * for creating all-day events that is part of the extra data of the intent. 102 * This is used only for creating new events and is set to true if 103 * the default for the new event should be an all-day event. 104 */ 105 public static final String EVENT_ALL_DAY = "allDay"; 106 107 private static final int MAX_REMINDERS = 5; 108 109 private static final int MENU_GROUP_REMINDER = 1; 110 private static final int MENU_GROUP_SHOW_OPTIONS = 2; 111 private static final int MENU_GROUP_HIDE_OPTIONS = 3; 112 113 private static final int MENU_ADD_REMINDER = 1; 114 private static final int MENU_SHOW_EXTRA_OPTIONS = 2; 115 private static final int MENU_HIDE_EXTRA_OPTIONS = 3; 116 117 private static final String[] EVENT_PROJECTION = new String[] { 118 Events._ID, // 0 119 Events.TITLE, // 1 120 Events.DESCRIPTION, // 2 121 Events.EVENT_LOCATION, // 3 122 Events.ALL_DAY, // 4 123 Events.HAS_ALARM, // 5 124 Events.CALENDAR_ID, // 6 125 Events.DTSTART, // 7 126 Events.DURATION, // 8 127 Events.EVENT_TIMEZONE, // 9 128 Events.RRULE, // 10 129 Events._SYNC_ID, // 11 130 Events.TRANSPARENCY, // 12 131 Events.VISIBILITY, // 13 132 Events.OWNER_ACCOUNT, // 14 133 Events.HAS_ATTENDEE_DATA, // 15 134 }; 135 private static final int EVENT_INDEX_ID = 0; 136 private static final int EVENT_INDEX_TITLE = 1; 137 private static final int EVENT_INDEX_DESCRIPTION = 2; 138 private static final int EVENT_INDEX_EVENT_LOCATION = 3; 139 private static final int EVENT_INDEX_ALL_DAY = 4; 140 private static final int EVENT_INDEX_HAS_ALARM = 5; 141 private static final int EVENT_INDEX_CALENDAR_ID = 6; 142 private static final int EVENT_INDEX_DTSTART = 7; 143 private static final int EVENT_INDEX_DURATION = 8; 144 private static final int EVENT_INDEX_TIMEZONE = 9; 145 private static final int EVENT_INDEX_RRULE = 10; 146 private static final int EVENT_INDEX_SYNC_ID = 11; 147 private static final int EVENT_INDEX_TRANSPARENCY = 12; 148 private static final int EVENT_INDEX_VISIBILITY = 13; 149 private static final int EVENT_INDEX_OWNER_ACCOUNT = 14; 150 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 15; 151 152 private static final String[] CALENDARS_PROJECTION = new String[] { 153 Calendars._ID, // 0 154 Calendars.DISPLAY_NAME, // 1 155 Calendars.OWNER_ACCOUNT, // 2 156 Calendars.COLOR, // 3 157 }; 158 private static final int CALENDARS_INDEX_DISPLAY_NAME = 1; 159 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 160 private static final int CALENDARS_INDEX_COLOR = 3; 161 private static final String CALENDARS_WHERE = Calendars.ACCESS_LEVEL + ">=" + 162 Calendars.CONTRIBUTOR_ACCESS + " AND " + Calendars.SYNC_EVENTS + "=1"; 163 164 private static final String[] REMINDERS_PROJECTION = new String[] { 165 Reminders._ID, // 0 166 Reminders.MINUTES, // 1 167 }; 168 private static final int REMINDERS_INDEX_MINUTES = 1; 169 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" + 170 Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" + 171 Reminders.METHOD_DEFAULT + ")"; 172 173 private static final String[] ATTENDEES_PROJECTION = new String[] { 174 Attendees.ATTENDEE_NAME, // 0 175 Attendees.ATTENDEE_EMAIL, // 1 176 }; 177 private static final int ATTENDEES_INDEX_NAME = 0; 178 private static final int ATTENDEES_INDEX_EMAIL = 1; 179 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=? AND " 180 + Attendees.ATTENDEE_RELATIONSHIP + "<>" + Attendees.RELATIONSHIP_ORGANIZER; 181 private static final String ATTENDEES_DELETE_PREFIX = Attendees.EVENT_ID + "=? AND " + 182 Attendees.ATTENDEE_EMAIL + " IN ("; 183 184 private static final int DOES_NOT_REPEAT = 0; 185 private static final int REPEATS_DAILY = 1; 186 private static final int REPEATS_EVERY_WEEKDAY = 2; 187 private static final int REPEATS_WEEKLY_ON_DAY = 3; 188 private static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4; 189 private static final int REPEATS_MONTHLY_ON_DAY = 5; 190 private static final int REPEATS_YEARLY = 6; 191 private static final int REPEATS_CUSTOM = 7; 192 193 private static final int MODIFY_UNINITIALIZED = 0; 194 private static final int MODIFY_SELECTED = 1; 195 private static final int MODIFY_ALL = 2; 196 private static final int MODIFY_ALL_FOLLOWING = 3; 197 198 private static final int DAY_IN_SECONDS = 24 * 60 * 60; 199 200 private int mFirstDayOfWeek; // cached in onCreate 201 private Uri mUri; 202 private Cursor mEventCursor; 203 private Cursor mCalendarsCursor; 204 205 private Button mStartDateButton; 206 private Button mEndDateButton; 207 private Button mStartTimeButton; 208 private Button mEndTimeButton; 209 private Button mSaveButton; 210 private Button mDeleteButton; 211 private Button mDiscardButton; 212 private CheckBox mAllDayCheckBox; 213 private Spinner mCalendarsSpinner; 214 private Spinner mRepeatsSpinner; 215 private Spinner mAvailabilitySpinner; 216 private Spinner mVisibilitySpinner; 217 private TextView mTitleTextView; 218 private TextView mLocationTextView; 219 private TextView mDescriptionTextView; 220 private View mRemindersSeparator; 221 private LinearLayout mRemindersContainer; 222 private LinearLayout mExtraOptions; 223 private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>(); 224 private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0); 225 private Rfc822Validator mEmailValidator; 226 private MultiAutoCompleteTextView mAttendeesList; 227 private EmailAddressAdapter mAddressAdapter; 228 private String mOriginalAttendees = ""; 229 230 // Used to control the visibility of the Guests textview. Default to true 231 private boolean mHasAttendeeData = true; 232 233 private EventRecurrence mEventRecurrence = new EventRecurrence(); 234 private String mRrule; 235 private boolean mCalendarsQueryComplete; 236 private boolean mSaveAfterQueryComplete; 237 private ProgressDialog mLoadingCalendarsDialog; 238 private AlertDialog mNoCalendarsDialog; 239 private ContentValues mInitialValues; 240 private String mOwnerAccount; 241 242 /** 243 * If the repeating event is created on the phone and it hasn't been 244 * synced yet to the web server, then there is a bug where you can't 245 * delete or change an instance of the repeating event. This case 246 * can be detected with mSyncId. If mSyncId == null, then the repeating 247 * event has not been synced to the phone, in which case we won't allow 248 * the user to change one instance. 249 */ 250 private String mSyncId; 251 252 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0); 253 private ArrayList<Integer> mReminderValues; 254 private ArrayList<String> mReminderLabels; 255 256 private Time mStartTime; 257 private Time mEndTime; 258 private int mModification = MODIFY_UNINITIALIZED; 259 private int mDefaultReminderMinutes; 260 261 private DeleteEventHelper mDeleteEventHelper; 262 private QueryHandler mQueryHandler; 263 264 /* This class is used to update the time buttons. */ 265 private class TimeListener implements OnTimeSetListener { 266 private View mView; 267 268 public TimeListener(View view) { 269 mView = view; 270 } 271 272 public void onTimeSet(TimePicker view, int hourOfDay, int minute) { 273 // Cache the member variables locally to avoid inner class overhead. 274 Time startTime = mStartTime; 275 Time endTime = mEndTime; 276 277 // Cache the start and end millis so that we limit the number 278 // of calls to normalize() and toMillis(), which are fairly 279 // expensive. 280 long startMillis; 281 long endMillis; 282 if (mView == mStartTimeButton) { 283 // The start time was changed. 284 int hourDuration = endTime.hour - startTime.hour; 285 int minuteDuration = endTime.minute - startTime.minute; 286 287 startTime.hour = hourOfDay; 288 startTime.minute = minute; 289 startMillis = startTime.normalize(true); 290 291 // Also update the end time to keep the duration constant. 292 endTime.hour = hourOfDay + hourDuration; 293 endTime.minute = minute + minuteDuration; 294 } else { 295 // The end time was changed. 296 startMillis = startTime.toMillis(true); 297 endTime.hour = hourOfDay; 298 endTime.minute = minute; 299 300 // Move to the next day if the end time is before the start time. 301 if (endTime.before(startTime)) { 302 endTime.monthDay = startTime.monthDay + 1; 303 } 304 } 305 306 endMillis = endTime.normalize(true); 307 308 setDate(mEndDateButton, endMillis); 309 setTime(mStartTimeButton, startMillis); 310 setTime(mEndTimeButton, endMillis); 311 } 312 } 313 314 private class TimeClickListener implements View.OnClickListener { 315 private Time mTime; 316 317 public TimeClickListener(Time time) { 318 mTime = time; 319 } 320 321 public void onClick(View v) { 322 new TimePickerDialog(EditEvent.this, new TimeListener(v), 323 mTime.hour, mTime.minute, 324 DateFormat.is24HourFormat(EditEvent.this)).show(); 325 } 326 } 327 328 private class DateListener implements OnDateSetListener { 329 View mView; 330 331 public DateListener(View view) { 332 mView = view; 333 } 334 335 public void onDateSet(DatePicker view, int year, int month, int monthDay) { 336 // Cache the member variables locally to avoid inner class overhead. 337 Time startTime = mStartTime; 338 Time endTime = mEndTime; 339 340 // Cache the start and end millis so that we limit the number 341 // of calls to normalize() and toMillis(), which are fairly 342 // expensive. 343 long startMillis; 344 long endMillis; 345 if (mView == mStartDateButton) { 346 // The start date was changed. 347 int yearDuration = endTime.year - startTime.year; 348 int monthDuration = endTime.month - startTime.month; 349 int monthDayDuration = endTime.monthDay - startTime.monthDay; 350 351 startTime.year = year; 352 startTime.month = month; 353 startTime.monthDay = monthDay; 354 startMillis = startTime.normalize(true); 355 356 // Also update the end date to keep the duration constant. 357 endTime.year = year + yearDuration; 358 endTime.month = month + monthDuration; 359 endTime.monthDay = monthDay + monthDayDuration; 360 endMillis = endTime.normalize(true); 361 362 // If the start date has changed then update the repeats. 363 populateRepeats(); 364 } else { 365 // The end date was changed. 366 startMillis = startTime.toMillis(true); 367 endTime.year = year; 368 endTime.month = month; 369 endTime.monthDay = monthDay; 370 endMillis = endTime.normalize(true); 371 372 // Do not allow an event to have an end time before the start time. 373 if (endTime.before(startTime)) { 374 endTime.set(startTime); 375 endMillis = startMillis; 376 } 377 } 378 379 setDate(mStartDateButton, startMillis); 380 setDate(mEndDateButton, endMillis); 381 setTime(mEndTimeButton, endMillis); // In case end time had to be reset 382 } 383 } 384 385 private class DateClickListener implements View.OnClickListener { 386 private Time mTime; 387 388 public DateClickListener(Time time) { 389 mTime = time; 390 } 391 392 public void onClick(View v) { 393 new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year, 394 mTime.month, mTime.monthDay).show(); 395 } 396 } 397 398 static private class CalendarsAdapter extends ResourceCursorAdapter { 399 public CalendarsAdapter(Context context, Cursor c) { 400 super(context, R.layout.calendars_item, c); 401 setDropDownViewResource(R.layout.calendars_dropdown_item); 402 } 403 404 @Override 405 public void bindView(View view, Context context, Cursor cursor) { 406 View colorBar = view.findViewById(R.id.color); 407 if (colorBar != null) { 408 colorBar.setBackgroundDrawable( 409 Utils.getColorChip(cursor.getInt(CALENDARS_INDEX_COLOR))); 410 } 411 412 TextView name = (TextView) view.findViewById(R.id.calendar_name); 413 if (name != null) { 414 String displayName = cursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 415 name.setText(displayName); 416 name.setTextColor(0xFF000000); 417 418 TextView accountName = (TextView) view.findViewById(R.id.account_name); 419 if(accountName != null) { 420 Resources res = context.getResources(); 421 accountName.setText(cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT)); 422 accountName.setVisibility(TextView.VISIBLE); 423 accountName.setTextColor(res.getColor(R.color.calendar_owner_text_color)); 424 } 425 } 426 } 427 } 428 429 // This is called if the user clicks on one of the buttons: "Save", 430 // "Discard", or "Delete". This is also called if the user clicks 431 // on the "remove reminder" button. 432 public void onClick(View v) { 433 if (v == mSaveButton) { 434 if (save()) { 435 finish(); 436 } 437 return; 438 } 439 440 if (v == mDeleteButton) { 441 long begin = mStartTime.toMillis(false /* use isDst */); 442 long end = mEndTime.toMillis(false /* use isDst */); 443 int which = -1; 444 switch (mModification) { 445 case MODIFY_SELECTED: 446 which = DeleteEventHelper.DELETE_SELECTED; 447 break; 448 case MODIFY_ALL_FOLLOWING: 449 which = DeleteEventHelper.DELETE_ALL_FOLLOWING; 450 break; 451 case MODIFY_ALL: 452 which = DeleteEventHelper.DELETE_ALL; 453 break; 454 } 455 mDeleteEventHelper.delete(begin, end, mEventCursor, which); 456 return; 457 } 458 459 if (v == mDiscardButton) { 460 finish(); 461 return; 462 } 463 464 // This must be a click on one of the "remove reminder" buttons 465 LinearLayout reminderItem = (LinearLayout) v.getParent(); 466 LinearLayout parent = (LinearLayout) reminderItem.getParent(); 467 parent.removeView(reminderItem); 468 mReminderItems.remove(reminderItem); 469 updateRemindersVisibility(); 470 } 471 472 // This is called if the user cancels a popup dialog. There are two 473 // dialogs: the "Loading calendars" dialog, and the "No calendars" 474 // dialog. The "Loading calendars" dialog is shown if there is a delay 475 // in loading the calendars (needed when creating an event) and the user 476 // tries to save the event before the calendars have finished loading. 477 // The "No calendars" dialog is shown if there are no syncable calendars. 478 public void onCancel(DialogInterface dialog) { 479 if (dialog == mLoadingCalendarsDialog) { 480 mSaveAfterQueryComplete = false; 481 } else if (dialog == mNoCalendarsDialog) { 482 finish(); 483 } 484 } 485 486 // This is called if the user clicks on a dialog button. 487 public void onClick(DialogInterface dialog, int which) { 488 if (dialog == mNoCalendarsDialog) { 489 finish(); 490 } 491 } 492 493 private class QueryHandler extends AsyncQueryHandler { 494 public QueryHandler(ContentResolver cr) { 495 super(cr); 496 } 497 498 @Override 499 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 500 // If the query didn't return a cursor for some reason return 501 if (cursor == null) { 502 return; 503 } 504 505 // If the Activity is finishing, then close the cursor. 506 // Otherwise, use the new cursor in the adapter. 507 if (isFinishing()) { 508 stopManagingCursor(cursor); 509 cursor.close(); 510 } else { 511 mCalendarsCursor = cursor; 512 startManagingCursor(cursor); 513 514 // Stop the spinner 515 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS, 516 Window.PROGRESS_VISIBILITY_OFF); 517 518 // If there are no syncable calendars, then we cannot allow 519 // creating a new event. 520 if (cursor.getCount() == 0) { 521 // Cancel the "loading calendars" dialog if it exists 522 if (mSaveAfterQueryComplete) { 523 mLoadingCalendarsDialog.cancel(); 524 } 525 526 // Create an error message for the user that, when clicked, 527 // will exit this activity without saving the event. 528 AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this); 529 builder.setTitle(R.string.no_syncable_calendars) 530 .setIcon(android.R.drawable.ic_dialog_alert) 531 .setMessage(R.string.no_calendars_found) 532 .setPositiveButton(android.R.string.ok, EditEvent.this) 533 .setOnCancelListener(EditEvent.this); 534 mNoCalendarsDialog = builder.show(); 535 return; 536 } 537 538 int defaultCalendarPosition = findDefaultCalendarPosition(mCalendarsCursor); 539 540 // populate the calendars spinner 541 CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor); 542 mCalendarsSpinner.setAdapter(adapter); 543 mCalendarsSpinner.setSelection(defaultCalendarPosition); 544 mCalendarsQueryComplete = true; 545 if (mSaveAfterQueryComplete) { 546 mLoadingCalendarsDialog.cancel(); 547 save(); 548 finish(); 549 } 550 551 // Find user domain and set it to the validator. 552 // TODO: we may want to update this validator if the user actually picks 553 // a different calendar. maybe not. depends on what we want for the 554 // user experience. this may change when we add support for multiple 555 // accounts, anyway. 556 if (mHasAttendeeData && cursor.moveToPosition(defaultCalendarPosition)) { 557 String ownEmail = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 558 if (ownEmail != null) { 559 String domain = extractDomain(ownEmail); 560 if (domain != null) { 561 mEmailValidator = new Rfc822Validator(domain); 562 mAttendeesList.setValidator(mEmailValidator); 563 } 564 } 565 } 566 } 567 } 568 569 // Find the calendar position in the cursor that matches calendar in preference 570 private int findDefaultCalendarPosition(Cursor calendarsCursor) { 571 if (calendarsCursor.getCount() <= 0) { 572 return -1; 573 } 574 575 String defaultCalendar = Utils.getSharedPreference(EditEvent.this, 576 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, null); 577 578 if (defaultCalendar == null) { 579 return 0; 580 } 581 582 int position = 0; 583 calendarsCursor.moveToPosition(-1); 584 while(calendarsCursor.moveToNext()) { 585 if (defaultCalendar.equals(mCalendarsCursor 586 .getString(CALENDARS_INDEX_OWNER_ACCOUNT))) { 587 return position; 588 } 589 position++; 590 } 591 return 0; 592 } 593 } 594 595 private static String extractDomain(String email) { 596 int separator = email.lastIndexOf('@'); 597 if (separator != -1 && ++separator < email.length()) { 598 return email.substring(separator); 599 } 600 return null; 601 } 602 603 @Override 604 protected void onCreate(Bundle icicle) { 605 super.onCreate(icicle); 606 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 607 setContentView(R.layout.edit_event); 608 609 boolean newEvent = false; 610 611 mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek(); 612 613 mStartTime = new Time(); 614 mEndTime = new Time(); 615 616 Intent intent = getIntent(); 617 mUri = intent.getData(); 618 619 if (mUri != null) { 620 mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null, null); 621 if (mEventCursor == null || mEventCursor.getCount() == 0) { 622 // The cursor is empty. This can happen if the event was deleted. 623 finish(); 624 return; 625 } 626 } 627 628 long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0); 629 long end = intent.getLongExtra(EVENT_END_TIME, 0); 630 631 String domain = "gmail.com"; 632 633 boolean allDay = false; 634 if (mEventCursor != null) { 635 // The event already exists so fetch the all-day status 636 mEventCursor.moveToFirst(); 637 mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0; 638 allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 639 String rrule = mEventCursor.getString(EVENT_INDEX_RRULE); 640 String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE); 641 long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID); 642 mOwnerAccount = mEventCursor.getString(EVENT_INDEX_OWNER_ACCOUNT); 643 if (!TextUtils.isEmpty(mOwnerAccount)) { 644 String ownerDomain = extractDomain(mOwnerAccount); 645 if (ownerDomain != null) { 646 domain = ownerDomain; 647 } 648 } 649 650 // Remember the initial values 651 mInitialValues = new ContentValues(); 652 mInitialValues.put(EVENT_BEGIN_TIME, begin); 653 mInitialValues.put(EVENT_END_TIME, end); 654 mInitialValues.put(Events.ALL_DAY, allDay ? 1 : 0); 655 mInitialValues.put(Events.RRULE, rrule); 656 mInitialValues.put(Events.EVENT_TIMEZONE, timezone); 657 mInitialValues.put(Events.CALENDAR_ID, calendarId); 658 } else { 659 newEvent = true; 660 // We are creating a new event, so set the default from the 661 // intent (if specified). 662 allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false); 663 664 // Start the spinner 665 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS, 666 Window.PROGRESS_VISIBILITY_ON); 667 668 // Start a query in the background to read the list of calendars 669 mQueryHandler = new QueryHandler(getContentResolver()); 670 mQueryHandler.startQuery(0, null, Calendars.CONTENT_URI, CALENDARS_PROJECTION, 671 CALENDARS_WHERE, null /* selection args */, null /* sort order */); 672 } 673 674 // If the event is all-day, read the times in UTC timezone 675 if (begin != 0) { 676 if (allDay) { 677 String tz = mStartTime.timezone; 678 mStartTime.timezone = Time.TIMEZONE_UTC; 679 mStartTime.set(begin); 680 mStartTime.timezone = tz; 681 682 // Calling normalize to calculate isDst 683 mStartTime.normalize(true); 684 } else { 685 mStartTime.set(begin); 686 } 687 } 688 689 if (end != 0) { 690 if (allDay) { 691 String tz = mStartTime.timezone; 692 mEndTime.timezone = Time.TIMEZONE_UTC; 693 mEndTime.set(end); 694 mEndTime.timezone = tz; 695 696 // Calling normalize to calculate isDst 697 mEndTime.normalize(true); 698 } else { 699 mEndTime.set(end); 700 } 701 } 702 703 // cache all the widgets 704 mTitleTextView = (TextView) findViewById(R.id.title); 705 mLocationTextView = (TextView) findViewById(R.id.location); 706 mDescriptionTextView = (TextView) findViewById(R.id.description); 707 mStartDateButton = (Button) findViewById(R.id.start_date); 708 mEndDateButton = (Button) findViewById(R.id.end_date); 709 mStartTimeButton = (Button) findViewById(R.id.start_time); 710 mEndTimeButton = (Button) findViewById(R.id.end_time); 711 mAllDayCheckBox = (CheckBox) findViewById(R.id.is_all_day); 712 mCalendarsSpinner = (Spinner) findViewById(R.id.calendars); 713 mRepeatsSpinner = (Spinner) findViewById(R.id.repeats); 714 mAvailabilitySpinner = (Spinner) findViewById(R.id.availability); 715 mVisibilitySpinner = (Spinner) findViewById(R.id.visibility); 716 mRemindersSeparator = findViewById(R.id.reminders_separator); 717 mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container); 718 mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container); 719 720 if (mHasAttendeeData) { 721 mAddressAdapter = new EmailAddressAdapter(this); 722 mEmailValidator = new Rfc822Validator(domain); 723 mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees); 724 } else { 725 findViewById(R.id.attendees_group).setVisibility(View.GONE); 726 } 727 728 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 729 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 730 if (isChecked) { 731 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 732 mEndTime.monthDay--; 733 long endMillis = mEndTime.normalize(true); 734 735 // Do not allow an event to have an end time before the start time. 736 if (mEndTime.before(mStartTime)) { 737 mEndTime.set(mStartTime); 738 endMillis = mEndTime.normalize(true); 739 } 740 setDate(mEndDateButton, endMillis); 741 setTime(mEndTimeButton, endMillis); 742 } 743 744 mStartTimeButton.setVisibility(View.GONE); 745 mEndTimeButton.setVisibility(View.GONE); 746 } else { 747 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 748 mEndTime.monthDay++; 749 long endMillis = mEndTime.normalize(true); 750 setDate(mEndDateButton, endMillis); 751 setTime(mEndTimeButton, endMillis); 752 } 753 754 mStartTimeButton.setVisibility(View.VISIBLE); 755 mEndTimeButton.setVisibility(View.VISIBLE); 756 } 757 } 758 }); 759 760 if (allDay) { 761 mAllDayCheckBox.setChecked(true); 762 } else { 763 mAllDayCheckBox.setChecked(false); 764 } 765 766 mSaveButton = (Button) findViewById(R.id.save); 767 mSaveButton.setOnClickListener(this); 768 769 mDeleteButton = (Button) findViewById(R.id.delete); 770 mDeleteButton.setOnClickListener(this); 771 772 mDiscardButton = (Button) findViewById(R.id.discard); 773 mDiscardButton.setOnClickListener(this); 774 775 // Initialize the reminder values array. 776 Resources r = getResources(); 777 String[] strings = r.getStringArray(R.array.reminder_minutes_values); 778 int size = strings.length; 779 ArrayList<Integer> list = new ArrayList<Integer>(size); 780 for (int i = 0 ; i < size ; i++) { 781 list.add(Integer.parseInt(strings[i])); 782 } 783 mReminderValues = list; 784 String[] labels = r.getStringArray(R.array.reminder_minutes_labels); 785 mReminderLabels = new ArrayList<String>(Arrays.asList(labels)); 786 787 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(this); 788 String durationString = 789 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0"); 790 mDefaultReminderMinutes = Integer.parseInt(durationString); 791 792 if (newEvent && mDefaultReminderMinutes != 0) { 793 addReminder(this, this, mReminderItems, mReminderValues, 794 mReminderLabels, mDefaultReminderMinutes); 795 } 796 797 long eventId = (mEventCursor == null) ? -1 : mEventCursor.getLong(EVENT_INDEX_ID); 798 ContentResolver cr = getContentResolver(); 799 800 // Reminders cursor 801 boolean hasAlarm = (mEventCursor != null) 802 && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0); 803 if (hasAlarm) { 804 Uri uri = Reminders.CONTENT_URI; 805 String where = String.format(REMINDERS_WHERE, eventId); 806 Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null); 807 try { 808 // First pass: collect all the custom reminder minutes (e.g., 809 // a reminder of 8 minutes) into a global list. 810 while (reminderCursor.moveToNext()) { 811 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES); 812 EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes); 813 } 814 815 // Second pass: create the reminder spinners 816 reminderCursor.moveToPosition(-1); 817 while (reminderCursor.moveToNext()) { 818 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES); 819 mOriginalMinutes.add(minutes); 820 EditEvent.addReminder(this, this, mReminderItems, mReminderValues, 821 mReminderLabels, minutes); 822 } 823 } finally { 824 reminderCursor.close(); 825 } 826 } 827 updateRemindersVisibility(); 828 829 // Setup the + Add Reminder Button 830 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { 831 public void onClick(View v) { 832 addReminder(); 833 } 834 }; 835 ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add); 836 reminderRemoveButton.setOnClickListener(addReminderOnClickListener); 837 838 mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */); 839 840 // Attendees cursor 841 if (mHasAttendeeData && eventId != -1) { 842 Uri uri = Attendees.CONTENT_URI; 843 String[] whereArgs = {Long.toString(eventId)}; 844 Cursor attendeeCursor = cr.query(uri, ATTENDEES_PROJECTION, ATTENDEES_WHERE, whereArgs, 845 null); 846 try { 847 StringBuilder b = new StringBuilder(); 848 while (attendeeCursor.moveToNext()) { 849 String name = attendeeCursor.getString(ATTENDEES_INDEX_NAME); 850 String email = attendeeCursor.getString(ATTENDEES_INDEX_EMAIL); 851 if (email != null) { 852 if (name != null && name.length() > 0 && !name.equals(email)) { 853 b.append('"').append(name).append("\" "); 854 } 855 b.append('<').append(email).append(">, "); 856 } 857 } 858 if (b.length() > 0) { 859 mOriginalAttendees = b.toString(); 860 mAttendeesList.setText(mOriginalAttendees); 861 } 862 } finally { 863 attendeeCursor.close(); 864 } 865 } 866 if (mEventCursor == null) { 867 // Allow the intent to specify the fields in the event. 868 // This will allow other apps to create events easily. 869 initFromIntent(intent); 870 } 871 } 872 873 private LinkedHashSet<Rfc822Token> getAddressesFromList(MultiAutoCompleteTextView list) { 874 list.clearComposingText(); 875 LinkedHashSet<Rfc822Token> addresses = new LinkedHashSet<Rfc822Token>(); 876 Rfc822Tokenizer.tokenize(list.getText(), addresses); 877 878 // validate the emails, out of paranoia. they should already be 879 // validated on input, but drop any invalid emails just to be safe. 880 Iterator<Rfc822Token> addressIterator = addresses.iterator(); 881 while (addressIterator.hasNext()) { 882 Rfc822Token address = addressIterator.next(); 883 if (!mEmailValidator.isValid(address.getAddress())) { 884 Log.w(TAG, "Dropping invalid attendee email address: " + address); 885 addressIterator.remove(); 886 } 887 } 888 return addresses; 889 } 890 891 // From com.google.android.gm.ComposeActivity 892 private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) { 893 MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res); 894 list.setAdapter(mAddressAdapter); 895 list.setTokenizer(new Rfc822Tokenizer()); 896 list.setValidator(mEmailValidator); 897 898 // NOTE: assumes no other filters are set 899 list.setFilters(sRecipientFilters); 900 901 return list; 902 } 903 904 /** 905 * From com.google.android.gm.ComposeActivity 906 * Implements special address cleanup rules: 907 * The first space key entry following an "@" symbol that is followed by any combination 908 * of letters and symbols, including one+ dots and zero commas, should insert an extra 909 * comma (followed by the space). 910 */ 911 private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() }; 912 913 private void initFromIntent(Intent intent) { 914 String title = intent.getStringExtra(Events.TITLE); 915 if (title != null) { 916 mTitleTextView.setText(title); 917 } 918 919 String location = intent.getStringExtra(Events.EVENT_LOCATION); 920 if (location != null) { 921 mLocationTextView.setText(location); 922 } 923 924 String description = intent.getStringExtra(Events.DESCRIPTION); 925 if (description != null) { 926 mDescriptionTextView.setText(description); 927 } 928 929 int availability = intent.getIntExtra(Events.TRANSPARENCY, -1); 930 if (availability != -1) { 931 mAvailabilitySpinner.setSelection(availability); 932 } 933 934 int visibility = intent.getIntExtra(Events.VISIBILITY, -1); 935 if (visibility != -1) { 936 mVisibilitySpinner.setSelection(visibility); 937 } 938 939 String rrule = intent.getStringExtra(Events.RRULE); 940 if (rrule != null) { 941 mRrule = rrule; 942 mEventRecurrence.parse(rrule); 943 } 944 } 945 946 @Override 947 protected void onResume() { 948 super.onResume(); 949 950 if (mUri != null) { 951 if (mEventCursor == null || mEventCursor.getCount() == 0) { 952 // The cursor is empty. This can happen if the event was deleted. 953 finish(); 954 return; 955 } 956 } 957 958 if (mEventCursor != null) { 959 Cursor cursor = mEventCursor; 960 cursor.moveToFirst(); 961 962 mRrule = cursor.getString(EVENT_INDEX_RRULE); 963 String title = cursor.getString(EVENT_INDEX_TITLE); 964 String description = cursor.getString(EVENT_INDEX_DESCRIPTION); 965 String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION); 966 int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY); 967 int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY); 968 if (visibility > 0) { 969 // For now we the array contains the values 0, 2, and 3. We subtract one to match. 970 visibility--; 971 } 972 973 if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) { 974 // If this event has not been synced, then don't allow deleting 975 // or changing a single instance. 976 mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID); 977 mEventRecurrence.parse(mRrule); 978 979 // If we haven't synced this repeating event yet, then don't 980 // allow the user to change just one instance. 981 int itemIndex = 0; 982 CharSequence[] items; 983 if (mSyncId == null) { 984 if(isFirstEventInSeries()) { 985 // Still display the option so the user knows all events are changing 986 items = new CharSequence[1]; 987 } else { 988 items = new CharSequence[2]; 989 } 990 } else { 991 if(isFirstEventInSeries()) { 992 items = new CharSequence[2]; 993 } else { 994 items = new CharSequence[3]; 995 } 996 items[itemIndex++] = getText(R.string.modify_event); 997 } 998 items[itemIndex++] = getText(R.string.modify_all); 999 1000 // Do one more check to make sure this remains at the end of the list 1001 if(!isFirstEventInSeries()) { 1002 // TODO Find out why modify all following causes a dup of the first event if 1003 // it's operating on the first event. 1004 items[itemIndex++] = getText(R.string.modify_all_following); 1005 } 1006 1007 // Display the modification dialog. 1008 new AlertDialog.Builder(this) 1009 .setOnCancelListener(new OnCancelListener() { 1010 public void onCancel(DialogInterface dialog) { 1011 finish(); 1012 } 1013 }) 1014 .setTitle(R.string.edit_event_label) 1015 .setItems(items, new OnClickListener() { 1016 public void onClick(DialogInterface dialog, int which) { 1017 if (which == 0) { 1018 mModification = 1019 (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED; 1020 } else if (which == 1) { 1021 mModification = 1022 (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL; 1023 } else if (which == 2) { 1024 mModification = MODIFY_ALL_FOLLOWING; 1025 } 1026 1027 // If we are modifying all the events in a 1028 // series then disable and ignore the date. 1029 if (mModification == MODIFY_ALL) { 1030 mStartDateButton.setEnabled(false); 1031 mEndDateButton.setEnabled(false); 1032 } else if (mModification == MODIFY_SELECTED) { 1033 mRepeatsSpinner.setEnabled(false); 1034 } 1035 } 1036 }) 1037 .show(); 1038 } 1039 1040 mTitleTextView.setText(title); 1041 mLocationTextView.setText(location); 1042 mDescriptionTextView.setText(description); 1043 mAvailabilitySpinner.setSelection(availability); 1044 mVisibilitySpinner.setSelection(visibility); 1045 1046 // This is an existing event so hide the calendar spinner 1047 // since we can't change the calendar. 1048 View calendarGroup = findViewById(R.id.calendar_group); 1049 calendarGroup.setVisibility(View.GONE); 1050 } else { 1051 // New event 1052 if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) { 1053 mStartTime.setToNow(); 1054 1055 // Round the time to the nearest half hour. 1056 mStartTime.second = 0; 1057 int minute = mStartTime.minute; 1058 if (minute == 0) { 1059 // We are already on a half hour increment 1060 } else if (minute > 0 && minute <= 30) { 1061 mStartTime.minute = 30; 1062 } else { 1063 mStartTime.minute = 0; 1064 mStartTime.hour += 1; 1065 } 1066 1067 long startMillis = mStartTime.normalize(true /* ignore isDst */); 1068 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS); 1069 } 1070 1071 // Hide delete button 1072 mDeleteButton.setVisibility(View.GONE); 1073 } 1074 1075 updateRemindersVisibility(); 1076 populateWhen(); 1077 populateRepeats(); 1078 } 1079 1080 @Override 1081 public boolean onCreateOptionsMenu(Menu menu) { 1082 MenuItem item; 1083 item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0, 1084 R.string.add_new_reminder); 1085 item.setIcon(R.drawable.ic_menu_reminder); 1086 item.setAlphabeticShortcut('r'); 1087 1088 item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0, 1089 R.string.edit_event_show_extra_options); 1090 item.setIcon(R.drawable.ic_menu_show_list); 1091 item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0, 1092 R.string.edit_event_hide_extra_options); 1093 item.setIcon(R.drawable.ic_menu_show_list); 1094 1095 return super.onCreateOptionsMenu(menu); 1096 } 1097 1098 @Override 1099 public boolean onPrepareOptionsMenu(Menu menu) { 1100 if (mReminderItems.size() < MAX_REMINDERS) { 1101 menu.setGroupVisible(MENU_GROUP_REMINDER, true); 1102 menu.setGroupEnabled(MENU_GROUP_REMINDER, true); 1103 } else { 1104 menu.setGroupVisible(MENU_GROUP_REMINDER, false); 1105 menu.setGroupEnabled(MENU_GROUP_REMINDER, false); 1106 } 1107 1108 if (mExtraOptions.getVisibility() == View.VISIBLE) { 1109 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false); 1110 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true); 1111 } else { 1112 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true); 1113 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false); 1114 } 1115 1116 return super.onPrepareOptionsMenu(menu); 1117 } 1118 1119 private void addReminder() { 1120 // TODO: when adding a new reminder, make it different from the 1121 // last one in the list (if any). 1122 if (mDefaultReminderMinutes == 0) { 1123 addReminder(this, this, mReminderItems, mReminderValues, 1124 mReminderLabels, 10 /* minutes */); 1125 } else { 1126 addReminder(this, this, mReminderItems, mReminderValues, 1127 mReminderLabels, mDefaultReminderMinutes); 1128 } 1129 updateRemindersVisibility(); 1130 } 1131 1132 @Override 1133 public boolean onOptionsItemSelected(MenuItem item) { 1134 switch (item.getItemId()) { 1135 case MENU_ADD_REMINDER: 1136 addReminder(); 1137 return true; 1138 case MENU_SHOW_EXTRA_OPTIONS: 1139 mExtraOptions.setVisibility(View.VISIBLE); 1140 return true; 1141 case MENU_HIDE_EXTRA_OPTIONS: 1142 mExtraOptions.setVisibility(View.GONE); 1143 return true; 1144 } 1145 return super.onOptionsItemSelected(item); 1146 } 1147 1148 @Override 1149 public void onBackPressed() { 1150 // If we are creating a new event, do not create it if the 1151 // title, location and description are all empty, in order to 1152 // prevent accidental "no subject" event creations. 1153 if (mUri != null || !isEmpty()) { 1154 if (!save()) { 1155 // We cannot exit this activity because the calendars 1156 // are still loading. 1157 return; 1158 } 1159 } 1160 finish(); 1161 } 1162 1163 private void populateWhen() { 1164 long startMillis = mStartTime.toMillis(false /* use isDst */); 1165 long endMillis = mEndTime.toMillis(false /* use isDst */); 1166 setDate(mStartDateButton, startMillis); 1167 setDate(mEndDateButton, endMillis); 1168 1169 setTime(mStartTimeButton, startMillis); 1170 setTime(mEndTimeButton, endMillis); 1171 1172 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime)); 1173 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime)); 1174 1175 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime)); 1176 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime)); 1177 } 1178 1179 private void populateRepeats() { 1180 Time time = mStartTime; 1181 Resources r = getResources(); 1182 int resource = android.R.layout.simple_spinner_item; 1183 1184 String[] days = new String[] { 1185 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM), 1186 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM), 1187 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM), 1188 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM), 1189 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM), 1190 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM), 1191 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM), 1192 }; 1193 String[] ordinals = r.getStringArray(R.array.ordinal_labels); 1194 1195 // Only display "Custom" in the spinner if the device does not support the 1196 // recurrence functionality of the event. Only display every weekday if 1197 // the event starts on a weekday. 1198 boolean isCustomRecurrence = isCustomRecurrence(); 1199 boolean isWeekdayEvent = isWeekdayEvent(); 1200 1201 ArrayList<String> repeatArray = new ArrayList<String>(0); 1202 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0); 1203 1204 repeatArray.add(r.getString(R.string.does_not_repeat)); 1205 recurrenceIndexes.add(DOES_NOT_REPEAT); 1206 1207 repeatArray.add(r.getString(R.string.daily)); 1208 recurrenceIndexes.add(REPEATS_DAILY); 1209 1210 if (isWeekdayEvent) { 1211 repeatArray.add(r.getString(R.string.every_weekday)); 1212 recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY); 1213 } 1214 1215 String format = r.getString(R.string.weekly); 1216 repeatArray.add(String.format(format, time.format("%A"))); 1217 recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY); 1218 1219 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day. 1220 int dayNumber = (time.monthDay - 1) / 7; 1221 format = r.getString(R.string.monthly_on_day_count); 1222 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay])); 1223 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT); 1224 1225 format = r.getString(R.string.monthly_on_day); 1226 repeatArray.add(String.format(format, time.monthDay)); 1227 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY); 1228 1229 long when = time.toMillis(false); 1230 format = r.getString(R.string.yearly); 1231 int flags = 0; 1232 if (DateFormat.is24HourFormat(this)) { 1233 flags |= DateUtils.FORMAT_24HOUR; 1234 } 1235 repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags))); 1236 recurrenceIndexes.add(REPEATS_YEARLY); 1237 1238 if (isCustomRecurrence) { 1239 repeatArray.add(r.getString(R.string.custom)); 1240 recurrenceIndexes.add(REPEATS_CUSTOM); 1241 } 1242 mRecurrenceIndexes = recurrenceIndexes; 1243 1244 int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT); 1245 if (mRrule != null) { 1246 if (isCustomRecurrence) { 1247 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM); 1248 } else { 1249 switch (mEventRecurrence.freq) { 1250 case EventRecurrence.DAILY: 1251 position = recurrenceIndexes.indexOf(REPEATS_DAILY); 1252 break; 1253 case EventRecurrence.WEEKLY: 1254 if (mEventRecurrence.repeatsOnEveryWeekDay()) { 1255 position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY); 1256 } else { 1257 position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY); 1258 } 1259 break; 1260 case EventRecurrence.MONTHLY: 1261 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 1262 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT); 1263 } else { 1264 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY); 1265 } 1266 break; 1267 case EventRecurrence.YEARLY: 1268 position = recurrenceIndexes.indexOf(REPEATS_YEARLY); 1269 break; 1270 } 1271 } 1272 } 1273 ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray); 1274 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 1275 mRepeatsSpinner.setAdapter(adapter); 1276 mRepeatsSpinner.setSelection(position); 1277 } 1278 1279 // Adds a reminder to the displayed list of reminders. 1280 // Returns true if successfully added reminder, false if no reminders can 1281 // be added. 1282 static boolean addReminder(Activity activity, View.OnClickListener listener, 1283 ArrayList<LinearLayout> items, ArrayList<Integer> values, 1284 ArrayList<String> labels, int minutes) { 1285 1286 if (items.size() >= MAX_REMINDERS) { 1287 return false; 1288 } 1289 1290 LayoutInflater inflater = activity.getLayoutInflater(); 1291 LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container); 1292 LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null); 1293 parent.addView(reminderItem); 1294 1295 Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value); 1296 Resources res = activity.getResources(); 1297 spinner.setPrompt(res.getString(R.string.reminders_label)); 1298 int resource = android.R.layout.simple_spinner_item; 1299 ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels); 1300 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 1301 spinner.setAdapter(adapter); 1302 1303 ImageButton reminderRemoveButton; 1304 reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove); 1305 reminderRemoveButton.setOnClickListener(listener); 1306 1307 int index = findMinutesInReminderList(values, minutes); 1308 spinner.setSelection(index); 1309 items.add(reminderItem); 1310 1311 return true; 1312 } 1313 1314 static void addMinutesToList(Context context, ArrayList<Integer> values, 1315 ArrayList<String> labels, int minutes) { 1316 int index = values.indexOf(minutes); 1317 if (index != -1) { 1318 return; 1319 } 1320 1321 // The requested "minutes" does not exist in the list, so insert it 1322 // into the list. 1323 1324 String label = constructReminderLabel(context, minutes, false); 1325 int len = values.size(); 1326 for (int i = 0; i < len; i++) { 1327 if (minutes < values.get(i)) { 1328 values.add(i, minutes); 1329 labels.add(i, label); 1330 return; 1331 } 1332 } 1333 1334 values.add(minutes); 1335 labels.add(len, label); 1336 } 1337 1338 /** 1339 * Finds the index of the given "minutes" in the "values" list. 1340 * 1341 * @param values the list of minutes corresponding to the spinner choices 1342 * @param minutes the minutes to search for in the values list 1343 * @return the index of "minutes" in the "values" list 1344 */ 1345 private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) { 1346 int index = values.indexOf(minutes); 1347 if (index == -1) { 1348 // This should never happen. 1349 Log.e("Cal", "Cannot find minutes (" + minutes + ") in list"); 1350 return 0; 1351 } 1352 return index; 1353 } 1354 1355 // Constructs a label given an arbitrary number of minutes. For example, 1356 // if the given minutes is 63, then this returns the string "63 minutes". 1357 // As another example, if the given minutes is 120, then this returns 1358 // "2 hours". 1359 static String constructReminderLabel(Context context, int minutes, boolean abbrev) { 1360 Resources resources = context.getResources(); 1361 int value, resId; 1362 1363 if (minutes % 60 != 0) { 1364 value = minutes; 1365 if (abbrev) { 1366 resId = R.plurals.Nmins; 1367 } else { 1368 resId = R.plurals.Nminutes; 1369 } 1370 } else if (minutes % (24 * 60) != 0) { 1371 value = minutes / 60; 1372 resId = R.plurals.Nhours; 1373 } else { 1374 value = minutes / ( 24 * 60); 1375 resId = R.plurals.Ndays; 1376 } 1377 1378 String format = resources.getQuantityString(resId, value); 1379 return String.format(format, value); 1380 } 1381 1382 private void updateRemindersVisibility() { 1383 if (mReminderItems.size() == 0) { 1384 mRemindersSeparator.setVisibility(View.GONE); 1385 mRemindersContainer.setVisibility(View.GONE); 1386 } else { 1387 mRemindersSeparator.setVisibility(View.VISIBLE); 1388 mRemindersContainer.setVisibility(View.VISIBLE); 1389 } 1390 } 1391 1392 private void setDate(TextView view, long millis) { 1393 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | 1394 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH | 1395 DateUtils.FORMAT_ABBREV_WEEKDAY; 1396 view.setText(DateUtils.formatDateTime(this, millis, flags)); 1397 } 1398 1399 private void setTime(TextView view, long millis) { 1400 int flags = DateUtils.FORMAT_SHOW_TIME; 1401 if (DateFormat.is24HourFormat(this)) { 1402 flags |= DateUtils.FORMAT_24HOUR; 1403 } 1404 view.setText(DateUtils.formatDateTime(this, millis, flags)); 1405 } 1406 1407 // Saves the event. Returns true if it is okay to exit this activity. 1408 private boolean save() { 1409 boolean forceSaveReminders = false; 1410 1411 // If we are creating a new event, then make sure we wait until the 1412 // query to fetch the list of calendars has finished. 1413 if (mEventCursor == null) { 1414 if (!mCalendarsQueryComplete) { 1415 // Wait for the calendars query to finish. 1416 if (mLoadingCalendarsDialog == null) { 1417 // Create the progress dialog 1418 mLoadingCalendarsDialog = ProgressDialog.show(this, 1419 getText(R.string.loading_calendars_title), 1420 getText(R.string.loading_calendars_message), 1421 true, true, this); 1422 mSaveAfterQueryComplete = true; 1423 } 1424 return false; 1425 } 1426 1427 // Avoid creating a new event if the calendars cursor is empty or we clicked through 1428 // too quickly and no calendar was selected (blame the monkey) 1429 if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0 || 1430 mCalendarsSpinner.getSelectedItemId() == AdapterView.INVALID_ROW_ID) { 1431 Log.w("Cal", "The calendars table does not contain any calendars" 1432 + " or no calendar was selected." 1433 + " New event was not created."); 1434 return true; 1435 } 1436 Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show(); 1437 } else { 1438 Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show(); 1439 } 1440 1441 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 1442 int eventIdIndex = -1; 1443 1444 ContentValues values = getContentValuesFromUi(); 1445 Uri uri = mUri; 1446 1447 // Update the "hasAlarm" field for the event 1448 ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems, 1449 mReminderValues); 1450 int len = reminderMinutes.size(); 1451 values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0); 1452 1453 // For recurring events, we must make sure that we use duration rather 1454 // than dtend. 1455 if (uri == null) { 1456 // Add hasAttendeeData for a new event 1457 values.put(Events.HAS_ATTENDEE_DATA, 1); 1458 // Create new event with new contents 1459 addRecurrenceRule(values); 1460 if (mRrule != null) { 1461 values.remove(Events.DTEND); 1462 } 1463 eventIdIndex = ops.size(); 1464 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values); 1465 ops.add(b.build()); 1466 forceSaveReminders = true; 1467 1468 } else if (mRrule == null) { 1469 // Modify contents of a non-repeating event 1470 addRecurrenceRule(values); 1471 checkTimeDependentFields(values); 1472 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); 1473 1474 } else if (mInitialValues.getAsString(Events.RRULE) == null) { 1475 // This event was changed from a non-repeating event to a 1476 // repeating event. 1477 addRecurrenceRule(values); 1478 values.remove(Events.DTEND); 1479 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); 1480 1481 } else if (mModification == MODIFY_SELECTED) { 1482 // Modify contents of the current instance of repeating event 1483 1484 // Create a recurrence exception 1485 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME); 1486 values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID)); 1487 values.put(Events.ORIGINAL_INSTANCE_TIME, begin); 1488 boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0; 1489 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0); 1490 1491 eventIdIndex = ops.size(); 1492 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values); 1493 ops.add(b.build()); 1494 forceSaveReminders = true; 1495 1496 } else if (mModification == MODIFY_ALL_FOLLOWING) { 1497 // Modify this instance and all future instances of repeating event 1498 addRecurrenceRule(values); 1499 1500 if (mRrule == null) { 1501 // We've changed a recurring event to a non-recurring event. 1502 // If the event we are editing is the first in the series, 1503 // then delete the whole series. Otherwise, update the series 1504 // to end at the new start time. 1505 if (isFirstEventInSeries()) { 1506 ops.add(ContentProviderOperation.newDelete(uri).build()); 1507 } else { 1508 // Update the current repeating event to end at the new 1509 // start time. 1510 updatePastEvents(ops, uri); 1511 } 1512 eventIdIndex = ops.size(); 1513 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values) 1514 .build()); 1515 } else { 1516 if (isFirstEventInSeries()) { 1517 checkTimeDependentFields(values); 1518 values.remove(Events.DTEND); 1519 Builder b = ContentProviderOperation.newUpdate(uri).withValues(values); 1520 ops.add(b.build()); 1521 } else { 1522 // Update the current repeating event to end at the new 1523 // start time. 1524 updatePastEvents(ops, uri); 1525 1526 // Create a new event with the user-modified fields 1527 values.remove(Events.DTEND); 1528 eventIdIndex = ops.size(); 1529 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues( 1530 values).build()); 1531 } 1532 } 1533 forceSaveReminders = true; 1534 1535 } else if (mModification == MODIFY_ALL) { 1536 1537 // Modify all instances of repeating event 1538 addRecurrenceRule(values); 1539 1540 if (mRrule == null) { 1541 // We've changed a recurring event to a non-recurring event. 1542 // Delete the whole series and replace it with a new 1543 // non-recurring event. 1544 ops.add(ContentProviderOperation.newDelete(uri).build()); 1545 1546 eventIdIndex = ops.size(); 1547 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values) 1548 .build()); 1549 forceSaveReminders = true; 1550 } else { 1551 checkTimeDependentFields(values); 1552 values.remove(Events.DTEND); 1553 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); 1554 } 1555 } 1556 1557 // New Event or New Exception to an existing event 1558 boolean newEvent = (eventIdIndex != -1); 1559 1560 if (newEvent) { 1561 saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes, 1562 forceSaveReminders); 1563 } else if (uri != null) { 1564 long eventId = ContentUris.parseId(uri); 1565 saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes, 1566 forceSaveReminders); 1567 } 1568 1569 Builder b; 1570 1571 // New event/instance - Set Organizer's response as yes 1572 if (mHasAttendeeData && newEvent) { 1573 values.clear(); 1574 int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition(); 1575 1576 // Save the default calendar for new events 1577 if (mCalendarsCursor != null) { 1578 if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) { 1579 String defaultCalendar = mCalendarsCursor 1580 .getString(CALENDARS_INDEX_OWNER_ACCOUNT); 1581 Utils.setSharedPreference(this, 1582 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, defaultCalendar); 1583 } 1584 } 1585 1586 String ownerEmail = mOwnerAccount; 1587 // Just in case mOwnerAccount is null, try to get owner from mCalendarsCursor 1588 if (ownerEmail == null && mCalendarsCursor != null && 1589 mCalendarsCursor.moveToPosition(calendarCursorPosition)) { 1590 ownerEmail = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 1591 } 1592 if (ownerEmail != null) { 1593 values.put(Attendees.ATTENDEE_EMAIL, ownerEmail); 1594 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER); 1595 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE); 1596 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED); 1597 1598 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI) 1599 .withValues(values); 1600 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex); 1601 ops.add(b.build()); 1602 } 1603 } 1604 1605 // TODO: is this the right test? this currently checks if this is 1606 // a new event or an existing event. or is this a paranoia check? 1607 if (mHasAttendeeData && (newEvent || uri != null)) { 1608 Editable attendeesText = mAttendeesList.getText(); 1609 // Hit the content provider only if this is a new event or the user has changed it 1610 if (newEvent || !mOriginalAttendees.equals(attendeesText.toString())) { 1611 // figure out which attendees need to be added and which ones 1612 // need to be deleted. use a linked hash set, so we maintain 1613 // order (but also remove duplicates). 1614 LinkedHashSet<Rfc822Token> newAttendees = getAddressesFromList(mAttendeesList); 1615 1616 // the eventId is only used if eventIdIndex is -1. 1617 // TODO: clean up this code. 1618 long eventId = uri != null ? ContentUris.parseId(uri) : -1; 1619 1620 // only compute deltas if this is an existing event. 1621 // new events (being inserted into the Events table) won't 1622 // have any existing attendees. 1623 if (!newEvent) { 1624 HashSet<Rfc822Token> removedAttendees = new HashSet<Rfc822Token>(); 1625 HashSet<Rfc822Token> originalAttendees = new HashSet<Rfc822Token>(); 1626 Rfc822Tokenizer.tokenize(mOriginalAttendees, originalAttendees); 1627 for (Rfc822Token originalAttendee : originalAttendees) { 1628 if (newAttendees.contains(originalAttendee)) { 1629 // existing attendee. remove from new attendees set. 1630 newAttendees.remove(originalAttendee); 1631 } else { 1632 // no longer in attendees. mark as removed. 1633 removedAttendees.add(originalAttendee); 1634 } 1635 } 1636 1637 // delete removed attendees 1638 b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI); 1639 1640 String[] args = new String[removedAttendees.size() + 1]; 1641 args[0] = Long.toString(eventId); 1642 int i = 1; 1643 StringBuilder deleteWhere = new StringBuilder(ATTENDEES_DELETE_PREFIX); 1644 for (Rfc822Token removedAttendee : removedAttendees) { 1645 if (i > 1) { 1646 deleteWhere.append(","); 1647 } 1648 deleteWhere.append("?"); 1649 args[i++] = removedAttendee.getAddress(); 1650 } 1651 deleteWhere.append(")"); 1652 b.withSelection(deleteWhere.toString(), args); 1653 ops.add(b.build()); 1654 } 1655 1656 if (newAttendees.size() > 0) { 1657 // Insert the new attendees 1658 for (Rfc822Token attendee : newAttendees) { 1659 values.clear(); 1660 values.put(Attendees.ATTENDEE_NAME, attendee.getName()); 1661 values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress()); 1662 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE); 1663 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE); 1664 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE); 1665 1666 if (newEvent) { 1667 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI) 1668 .withValues(values); 1669 b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex); 1670 } else { 1671 values.put(Attendees.EVENT_ID, eventId); 1672 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI) 1673 .withValues(values); 1674 } 1675 ops.add(b.build()); 1676 } 1677 } 1678 } 1679 } 1680 1681 try { 1682 // TODO Move this to background thread 1683 ContentProviderResult[] results = 1684 getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops); 1685 if (DEBUG) { 1686 for (int i = 0; i < results.length; i++) { 1687 Log.v(TAG, "results = " + results[i].toString()); 1688 } 1689 } 1690 } catch (RemoteException e) { 1691 Log.w(TAG, "Ignoring unexpected remote exception", e); 1692 } catch (OperationApplicationException e) { 1693 Log.w(TAG, "Ignoring unexpected exception", e); 1694 } 1695 1696 return true; 1697 } 1698 1699 private boolean isFirstEventInSeries() { 1700 int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART); 1701 long start = mEventCursor.getLong(dtStart); 1702 return start == mStartTime.toMillis(true); 1703 } 1704 1705 private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) { 1706 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART); 1707 String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION); 1708 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 1709 String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE); 1710 mEventRecurrence.parse(oldRrule); 1711 1712 Time untilTime = new Time(); 1713 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME); 1714 ContentValues oldValues = new ContentValues(); 1715 1716 // The "until" time must be in UTC time in order for Google calendar 1717 // to display it properly. For all-day events, the "until" time string 1718 // must include just the date field, and not the time field. The 1719 // repeating events repeat up to and including the "until" time. 1720 untilTime.timezone = Time.TIMEZONE_UTC; 1721 1722 // Subtract one second from the old begin time to get the new 1723 // "until" time. 1724 untilTime.set(begin - 1000); // subtract one second (1000 millis) 1725 if (allDay) { 1726 untilTime.hour = 0; 1727 untilTime.minute = 0; 1728 untilTime.second = 0; 1729 untilTime.allDay = true; 1730 untilTime.normalize(false); 1731 1732 // For all-day events, the duration must be in days, not seconds. 1733 // Otherwise, Google Calendar will (mistakenly) change this event 1734 // into a non-all-day event. 1735 int len = oldDuration.length(); 1736 if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') { 1737 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1)); 1738 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS; 1739 oldDuration = "P" + days + "D"; 1740 } 1741 } 1742 mEventRecurrence.until = untilTime.format2445(); 1743 1744 oldValues.put(Events.DTSTART, oldStartMillis); 1745 oldValues.put(Events.DURATION, oldDuration); 1746 oldValues.put(Events.RRULE, mEventRecurrence.toString()); 1747 Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues); 1748 ops.add(b.build()); 1749 } 1750 1751 private void checkTimeDependentFields(ContentValues values) { 1752 long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME); 1753 long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME); 1754 boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0; 1755 String oldRrule = mInitialValues.getAsString(Events.RRULE); 1756 String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE); 1757 1758 long newBegin = values.getAsLong(Events.DTSTART); 1759 long newEnd = values.getAsLong(Events.DTEND); 1760 boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0; 1761 String newRrule = values.getAsString(Events.RRULE); 1762 String newTimezone = values.getAsString(Events.EVENT_TIMEZONE); 1763 1764 // If none of the time-dependent fields changed, then remove them. 1765 if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay 1766 && TextUtils.equals(oldRrule, newRrule) 1767 && TextUtils.equals(oldTimezone, newTimezone)) { 1768 values.remove(Events.DTSTART); 1769 values.remove(Events.DTEND); 1770 values.remove(Events.DURATION); 1771 values.remove(Events.ALL_DAY); 1772 values.remove(Events.RRULE); 1773 values.remove(Events.EVENT_TIMEZONE); 1774 return; 1775 } 1776 1777 if (oldRrule == null || newRrule == null) { 1778 return; 1779 } 1780 1781 // If we are modifying all events then we need to set DTSTART to the 1782 // start time of the first event in the series, not the current 1783 // date and time. If the start time of the event was changed 1784 // (from, say, 3pm to 4pm), then we want to add the time difference 1785 // to the start time of the first event in the series (the DTSTART 1786 // value). If we are modifying one instance or all following instances, 1787 // then we leave the DTSTART field alone. 1788 if (mModification == MODIFY_ALL) { 1789 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART); 1790 if (oldBegin != newBegin) { 1791 // The user changed the start time of this event 1792 long offset = newBegin - oldBegin; 1793 oldStartMillis += offset; 1794 } 1795 values.put(Events.DTSTART, oldStartMillis); 1796 } 1797 } 1798 1799 static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems, 1800 ArrayList<Integer> reminderValues) { 1801 int len = reminderItems.size(); 1802 ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len); 1803 for (int index = 0; index < len; index++) { 1804 LinearLayout layout = reminderItems.get(index); 1805 Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value); 1806 int minutes = reminderValues.get(spinner.getSelectedItemPosition()); 1807 reminderMinutes.add(minutes); 1808 } 1809 return reminderMinutes; 1810 } 1811 1812 /** 1813 * Saves the reminders, if they changed. Returns true if the database 1814 * was updated. 1815 * 1816 * @param ops the array of ContentProviderOperations 1817 * @param eventId the id of the event whose reminders are being updated 1818 * @param reminderMinutes the array of reminders set by the user 1819 * @param originalMinutes the original array of reminders 1820 * @param forceSave if true, then save the reminders even if they didn't 1821 * change 1822 * @return true if the database was updated 1823 */ 1824 static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId, 1825 ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes, 1826 boolean forceSave) { 1827 // If the reminders have not changed, then don't update the database 1828 if (reminderMinutes.equals(originalMinutes) && !forceSave) { 1829 return false; 1830 } 1831 1832 // Delete all the existing reminders for this event 1833 String where = Reminders.EVENT_ID + "=?"; 1834 String[] args = new String[] { Long.toString(eventId) }; 1835 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI); 1836 b.withSelection(where, args); 1837 ops.add(b.build()); 1838 1839 ContentValues values = new ContentValues(); 1840 int len = reminderMinutes.size(); 1841 1842 // Insert the new reminders, if any 1843 for (int i = 0; i < len; i++) { 1844 int minutes = reminderMinutes.get(i); 1845 1846 values.clear(); 1847 values.put(Reminders.MINUTES, minutes); 1848 values.put(Reminders.METHOD, Reminders.METHOD_ALERT); 1849 values.put(Reminders.EVENT_ID, eventId); 1850 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values); 1851 ops.add(b.build()); 1852 } 1853 return true; 1854 } 1855 1856 static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops, 1857 int eventIdIndex, ArrayList<Integer> reminderMinutes, 1858 ArrayList<Integer> originalMinutes, boolean forceSave) { 1859 // If the reminders have not changed, then don't update the database 1860 if (reminderMinutes.equals(originalMinutes) && !forceSave) { 1861 return false; 1862 } 1863 1864 // Delete all the existing reminders for this event 1865 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI); 1866 b.withSelection(Reminders.EVENT_ID + "=?", new String[1]); 1867 b.withSelectionBackReference(0, eventIdIndex); 1868 ops.add(b.build()); 1869 1870 ContentValues values = new ContentValues(); 1871 int len = reminderMinutes.size(); 1872 1873 // Insert the new reminders, if any 1874 for (int i = 0; i < len; i++) { 1875 int minutes = reminderMinutes.get(i); 1876 1877 values.clear(); 1878 values.put(Reminders.MINUTES, minutes); 1879 values.put(Reminders.METHOD, Reminders.METHOD_ALERT); 1880 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values); 1881 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex); 1882 ops.add(b.build()); 1883 } 1884 return true; 1885 } 1886 1887 private void addRecurrenceRule(ContentValues values) { 1888 updateRecurrenceRule(); 1889 1890 if (mRrule == null) { 1891 return; 1892 } 1893 1894 values.put(Events.RRULE, mRrule); 1895 long end = mEndTime.toMillis(true /* ignore dst */); 1896 long start = mStartTime.toMillis(true /* ignore dst */); 1897 String duration; 1898 1899 boolean isAllDay = mAllDayCheckBox.isChecked(); 1900 if (isAllDay) { 1901 long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS; 1902 duration = "P" + days + "D"; 1903 } else { 1904 long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS; 1905 duration = "P" + seconds + "S"; 1906 } 1907 values.put(Events.DURATION, duration); 1908 } 1909 1910 private void clearRecurrence() { 1911 mEventRecurrence.byday = null; 1912 mEventRecurrence.bydayNum = null; 1913 mEventRecurrence.bydayCount = 0; 1914 mEventRecurrence.bymonth = null; 1915 mEventRecurrence.bymonthCount = 0; 1916 mEventRecurrence.bymonthday = null; 1917 mEventRecurrence.bymonthdayCount = 0; 1918 } 1919 1920 private void updateRecurrenceRule() { 1921 int position = mRepeatsSpinner.getSelectedItemPosition(); 1922 int selection = mRecurrenceIndexes.get(position); 1923 // Make sure we don't have any leftover data from the previous setting 1924 clearRecurrence(); 1925 1926 if (selection == DOES_NOT_REPEAT) { 1927 mRrule = null; 1928 return; 1929 } else if (selection == REPEATS_CUSTOM) { 1930 // Keep custom recurrence as before. 1931 return; 1932 } else if (selection == REPEATS_DAILY) { 1933 mEventRecurrence.freq = EventRecurrence.DAILY; 1934 } else if (selection == REPEATS_EVERY_WEEKDAY) { 1935 mEventRecurrence.freq = EventRecurrence.WEEKLY; 1936 int dayCount = 5; 1937 int[] byday = new int[dayCount]; 1938 int[] bydayNum = new int[dayCount]; 1939 1940 byday[0] = EventRecurrence.MO; 1941 byday[1] = EventRecurrence.TU; 1942 byday[2] = EventRecurrence.WE; 1943 byday[3] = EventRecurrence.TH; 1944 byday[4] = EventRecurrence.FR; 1945 for (int day = 0; day < dayCount; day++) { 1946 bydayNum[day] = 0; 1947 } 1948 1949 mEventRecurrence.byday = byday; 1950 mEventRecurrence.bydayNum = bydayNum; 1951 mEventRecurrence.bydayCount = dayCount; 1952 } else if (selection == REPEATS_WEEKLY_ON_DAY) { 1953 mEventRecurrence.freq = EventRecurrence.WEEKLY; 1954 int[] days = new int[1]; 1955 int dayCount = 1; 1956 int[] dayNum = new int[dayCount]; 1957 1958 days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay); 1959 // not sure why this needs to be zero, but set it for now. 1960 dayNum[0] = 0; 1961 1962 mEventRecurrence.byday = days; 1963 mEventRecurrence.bydayNum = dayNum; 1964 mEventRecurrence.bydayCount = dayCount; 1965 } else if (selection == REPEATS_MONTHLY_ON_DAY) { 1966 mEventRecurrence.freq = EventRecurrence.MONTHLY; 1967 mEventRecurrence.bydayCount = 0; 1968 mEventRecurrence.bymonthdayCount = 1; 1969 int[] bymonthday = new int[1]; 1970 bymonthday[0] = mStartTime.monthDay; 1971 mEventRecurrence.bymonthday = bymonthday; 1972 } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) { 1973 mEventRecurrence.freq = EventRecurrence.MONTHLY; 1974 mEventRecurrence.bydayCount = 1; 1975 mEventRecurrence.bymonthdayCount = 0; 1976 1977 int[] byday = new int[1]; 1978 int[] bydayNum = new int[1]; 1979 // Compute the week number (for example, the "2nd" Monday) 1980 int dayCount = 1 + ((mStartTime.monthDay - 1) / 7); 1981 if (dayCount == 5) { 1982 dayCount = -1; 1983 } 1984 bydayNum[0] = dayCount; 1985 byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay); 1986 mEventRecurrence.byday = byday; 1987 mEventRecurrence.bydayNum = bydayNum; 1988 } else if (selection == REPEATS_YEARLY) { 1989 mEventRecurrence.freq = EventRecurrence.YEARLY; 1990 } 1991 1992 // Set the week start day. 1993 mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek); 1994 mRrule = mEventRecurrence.toString(); 1995 } 1996 1997 private ContentValues getContentValuesFromUi() { 1998 String title = mTitleTextView.getText().toString().trim(); 1999 boolean isAllDay = mAllDayCheckBox.isChecked(); 2000 String location = mLocationTextView.getText().toString().trim(); 2001 String description = mDescriptionTextView.getText().toString().trim(); 2002 2003 ContentValues values = new ContentValues(); 2004 2005 String timezone = null; 2006 long startMillis; 2007 long endMillis; 2008 long calendarId; 2009 if (isAllDay) { 2010 // Reset start and end time, increment the monthDay by 1, and set 2011 // the timezone to UTC, as required for all-day events. 2012 timezone = Time.TIMEZONE_UTC; 2013 mStartTime.hour = 0; 2014 mStartTime.minute = 0; 2015 mStartTime.second = 0; 2016 mStartTime.timezone = timezone; 2017 startMillis = mStartTime.normalize(true); 2018 2019 mEndTime.hour = 0; 2020 mEndTime.minute = 0; 2021 mEndTime.second = 0; 2022 mEndTime.monthDay++; 2023 mEndTime.timezone = timezone; 2024 endMillis = mEndTime.normalize(true); 2025 2026 if (mEventCursor == null) { 2027 // This is a new event 2028 calendarId = mCalendarsSpinner.getSelectedItemId(); 2029 } else { 2030 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID); 2031 } 2032 } else { 2033 startMillis = mStartTime.toMillis(true); 2034 endMillis = mEndTime.toMillis(true); 2035 if (mEventCursor != null) { 2036 // This is an existing event 2037 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE); 2038 2039 // The timezone might be null if we are changing an existing 2040 // all-day event to a non-all-day event. We need to assign 2041 // a timezone to the non-all-day event. 2042 if (TextUtils.isEmpty(timezone)) { 2043 timezone = TimeZone.getDefault().getID(); 2044 } 2045 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID); 2046 } else { 2047 // This is a new event 2048 calendarId = mCalendarsSpinner.getSelectedItemId(); 2049 2050 // The timezone for a new event is the currently displayed 2051 // timezone, NOT the timezone of the containing calendar. 2052 timezone = TimeZone.getDefault().getID(); 2053 } 2054 } 2055 2056 values.put(Events.CALENDAR_ID, calendarId); 2057 values.put(Events.EVENT_TIMEZONE, timezone); 2058 values.put(Events.TITLE, title); 2059 values.put(Events.ALL_DAY, isAllDay ? 1 : 0); 2060 values.put(Events.DTSTART, startMillis); 2061 values.put(Events.DTEND, endMillis); 2062 values.put(Events.DESCRIPTION, description); 2063 values.put(Events.EVENT_LOCATION, location); 2064 values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition()); 2065 2066 int visibility = mVisibilitySpinner.getSelectedItemPosition(); 2067 if (visibility > 0) { 2068 // For now we the array contains the values 0, 2, and 3. We add one to match. 2069 visibility++; 2070 } 2071 values.put(Events.VISIBILITY, visibility); 2072 2073 return values; 2074 } 2075 2076 private boolean isEmpty() { 2077 String title = mTitleTextView.getText().toString().trim(); 2078 if (title.length() > 0) { 2079 return false; 2080 } 2081 2082 String location = mLocationTextView.getText().toString().trim(); 2083 if (location.length() > 0) { 2084 return false; 2085 } 2086 2087 String description = mDescriptionTextView.getText().toString().trim(); 2088 if (description.length() > 0) { 2089 return false; 2090 } 2091 2092 return true; 2093 } 2094 2095 private boolean isCustomRecurrence() { 2096 2097 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) { 2098 return true; 2099 } 2100 2101 if (mEventRecurrence.freq == 0) { 2102 return false; 2103 } 2104 2105 switch (mEventRecurrence.freq) { 2106 case EventRecurrence.DAILY: 2107 return false; 2108 case EventRecurrence.WEEKLY: 2109 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) { 2110 return false; 2111 } else if (mEventRecurrence.bydayCount == 1) { 2112 return false; 2113 } 2114 break; 2115 case EventRecurrence.MONTHLY: 2116 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 2117 return false; 2118 } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) { 2119 return false; 2120 } 2121 break; 2122 case EventRecurrence.YEARLY: 2123 return false; 2124 } 2125 2126 return true; 2127 } 2128 2129 private boolean isWeekdayEvent() { 2130 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) { 2131 return true; 2132 } 2133 return false; 2134 } 2135 } 2136