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