EditEventView.java revision d845fbe558d5229102b58cce70a1b29fe6cb6967
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.calendar.event; 18 19import com.android.calendar.CalendarEventModel; 20import com.android.calendar.CalendarEventModel.Attendee; 21import com.android.calendar.CalendarPreferenceActivity; 22import com.android.calendar.EmailAddressAdapter; 23import com.android.calendar.R; 24import com.android.calendar.TimezoneAdapter; 25import com.android.calendar.TimezoneAdapter.TimezoneRow; 26import com.android.calendar.Utils; 27import com.android.calendar.event.EditEventHelper.EditDoneRunnable; 28import com.android.common.Rfc822InputFilter; 29import com.android.common.Rfc822Validator; 30 31import android.app.Activity; 32import android.app.AlertDialog; 33import android.app.DatePickerDialog; 34import android.app.DatePickerDialog.OnDateSetListener; 35import android.app.ProgressDialog; 36import android.app.TimePickerDialog; 37import android.app.TimePickerDialog.OnTimeSetListener; 38import android.content.Context; 39import android.content.DialogInterface; 40import android.content.SharedPreferences; 41import android.content.res.Resources; 42import android.database.Cursor; 43import android.pim.EventRecurrence; 44import android.provider.Calendar.Calendars; 45import android.text.InputFilter; 46import android.text.TextUtils; 47import android.text.format.DateFormat; 48import android.text.format.DateUtils; 49import android.text.format.Time; 50import android.text.util.Rfc822Tokenizer; 51import android.view.LayoutInflater; 52import android.view.View; 53import android.widget.ArrayAdapter; 54import android.widget.Button; 55import android.widget.CheckBox; 56import android.widget.CompoundButton; 57import android.widget.DatePicker; 58import android.widget.ImageButton; 59import android.widget.LinearLayout; 60import android.widget.ListView; 61import android.widget.MultiAutoCompleteTextView; 62import android.widget.ResourceCursorAdapter; 63import android.widget.ScrollView; 64import android.widget.Spinner; 65import android.widget.TextView; 66import android.widget.TimePicker; 67 68import java.util.ArrayList; 69import java.util.Arrays; 70import java.util.Calendar; 71import java.util.HashMap; 72import java.util.TimeZone; 73 74public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener, 75 DialogInterface.OnClickListener { 76 77 private static final String TAG = EditEventView.class.getSimpleName(); 78 79 private static final int REMINDER_FLING_VELOCITY = 2000; 80 81 private LayoutInflater mLayoutInflater; 82 83 TextView mLoadingMessage; 84 ScrollView mScrollView; 85 Button mStartDateButton; 86 Button mEndDateButton; 87 Button mStartTimeButton; 88 Button mEndTimeButton; 89 Button mSaveButton; 90 Button mDeleteButton; 91 Button mDiscardButton; 92 Button mTimezoneButton; 93 CheckBox mAllDayCheckBox; 94 Spinner mCalendarsSpinner; 95 Spinner mRepeatsSpinner; 96 Spinner mTransparencySpinner; 97 Spinner mVisibilitySpinner; 98 Spinner mResponseSpinner; 99 TextView mTitleTextView; 100 TextView mLocationTextView; 101 TextView mDescriptionTextView; 102 TextView mTimezoneTextView; 103 TextView mTimezoneFooterView; 104 View mRemindersSeparator; 105 LinearLayout mRemindersContainer; 106 MultiAutoCompleteTextView mAttendeesList; 107 ImageButton mAddAttendeesButton; 108 ListView mGuestList; 109 AttendeesAdapter mAttendeesAdapter; 110 111 private ProgressDialog mLoadingCalendarsDialog; 112 private AlertDialog mNoCalendarsDialog; 113 private AlertDialog mTimezoneDialog; 114 private Activity mActivity; 115 private EditDoneRunnable mDone; 116 private View mView; 117 private CalendarEventModel mModel; 118 private Cursor mCalendarsCursor; 119 private EmailAddressAdapter mAddressAdapter; 120 private Rfc822Validator mEmailValidator; 121 private TimezoneAdapter mTimezoneAdapter; 122 123 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer>(0); 124 private ArrayList<Integer> mReminderValues; 125 private ArrayList<String> mReminderLabels; 126 private int mDefaultReminderMinutes; 127 128 private boolean mSaveAfterQueryComplete = false; 129 private boolean mCalendarsCursorSet = false; 130 131 private Time mStartTime; 132 private Time mEndTime; 133 private String mTimezone; 134 private int mModification = EditEventHelper.MODIFY_UNINITIALIZED; 135 136 private EventRecurrence mEventRecurrence = new EventRecurrence(); 137 138 private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0); 139 140 /* This class is used to update the time buttons. */ 141 private class TimeListener implements OnTimeSetListener { 142 private View mView; 143 144 public TimeListener(View view) { 145 mView = view; 146 } 147 148 public void onTimeSet(TimePicker view, int hourOfDay, int minute) { 149 // Cache the member variables locally to avoid inner class overhead. 150 Time startTime = mStartTime; 151 Time endTime = mEndTime; 152 153 // Cache the start and end millis so that we limit the number 154 // of calls to normalize() and toMillis(), which are fairly 155 // expensive. 156 long startMillis; 157 long endMillis; 158 if (mView == mStartTimeButton) { 159 // The start time was changed. 160 int hourDuration = endTime.hour - startTime.hour; 161 int minuteDuration = endTime.minute - startTime.minute; 162 163 startTime.hour = hourOfDay; 164 startTime.minute = minute; 165 startMillis = startTime.normalize(true); 166 167 // Also update the end time to keep the duration constant. 168 endTime.hour = hourOfDay + hourDuration; 169 endTime.minute = minute + minuteDuration; 170 } else { 171 // The end time was changed. 172 startMillis = startTime.toMillis(true); 173 endTime.hour = hourOfDay; 174 endTime.minute = minute; 175 176 // Move to the start time if the end time is before the start 177 // time. 178 if (endTime.before(startTime)) { 179 endTime.monthDay = startTime.monthDay + 1; 180 } 181 } 182 183 endMillis = endTime.normalize(true); 184 185 setDate(mEndDateButton, endMillis); 186 setTime(mStartTimeButton, startMillis); 187 setTime(mEndTimeButton, endMillis); 188 } 189 } 190 191 private class AddAttendeeClickListener implements View.OnClickListener { 192 @Override 193 public void onClick(View v) { 194 mAttendeesList.performValidation(); 195 mAttendeesAdapter.addAttendees(mAttendeesList.getText().toString()); 196 mAttendeesList.setText(""); 197 } 198 } 199 200 private class TimeClickListener implements View.OnClickListener { 201 private Time mTime; 202 203 public TimeClickListener(Time time) { 204 mTime = time; 205 } 206 207 public void onClick(View v) { 208 new TimePickerDialog(mActivity, new TimeListener(v), mTime.hour, mTime.minute, 209 DateFormat.is24HourFormat(mActivity)).show(); 210 } 211 } 212 213 private class DateListener implements OnDateSetListener { 214 View mView; 215 216 public DateListener(View view) { 217 mView = view; 218 } 219 220 public void onDateSet(DatePicker view, int year, int month, int monthDay) { 221 // Cache the member variables locally to avoid inner class overhead. 222 Time startTime = mStartTime; 223 Time endTime = mEndTime; 224 225 // Cache the start and end millis so that we limit the number 226 // of calls to normalize() and toMillis(), which are fairly 227 // expensive. 228 long startMillis; 229 long endMillis; 230 if (mView == mStartDateButton) { 231 // The start date was changed. 232 int yearDuration = endTime.year - startTime.year; 233 int monthDuration = endTime.month - startTime.month; 234 int monthDayDuration = endTime.monthDay - startTime.monthDay; 235 236 startTime.year = year; 237 startTime.month = month; 238 startTime.monthDay = monthDay; 239 startMillis = startTime.normalize(true); 240 241 // Also update the end date to keep the duration constant. 242 endTime.year = year + yearDuration; 243 endTime.month = month + monthDuration; 244 endTime.monthDay = monthDay + monthDayDuration; 245 endMillis = endTime.normalize(true); 246 247 // If the start date has changed then update the repeats. 248 populateRepeats(); 249 } else { 250 // The end date was changed. 251 startMillis = startTime.toMillis(true); 252 endTime.year = year; 253 endTime.month = month; 254 endTime.monthDay = monthDay; 255 endMillis = endTime.normalize(true); 256 257 // Do not allow an event to have an end time before the start 258 // time. 259 if (endTime.before(startTime)) { 260 endTime.set(startTime); 261 endMillis = startMillis; 262 } 263 } 264 265 setDate(mStartDateButton, startMillis); 266 setDate(mEndDateButton, endMillis); 267 setTime(mEndTimeButton, endMillis); // In case end time had to be 268 // reset 269 } 270 } 271 272 // Fills in the date and time fields 273 private void populateWhen() { 274 long startMillis = mStartTime.toMillis(false /* use isDst */); 275 long endMillis = mEndTime.toMillis(false /* use isDst */); 276 setDate(mStartDateButton, startMillis); 277 setDate(mEndDateButton, endMillis); 278 279 setTime(mStartTimeButton, startMillis); 280 setTime(mEndTimeButton, endMillis); 281 282 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime)); 283 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime)); 284 285 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime)); 286 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime)); 287 } 288 289 private void populateTimezone() { 290 mTimezoneButton.setOnClickListener(new View.OnClickListener() { 291 @Override 292 public void onClick(View v) { 293 showTimezoneDialog(); 294 } 295 }); 296 setTimezone(mTimezoneAdapter.getRowById(mTimezone)); 297 } 298 299 private void showTimezoneDialog() { 300 mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone); 301 AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); 302 builder.setTitle(R.string.timezone_label); 303 builder.setSingleChoiceItems( 304 mTimezoneAdapter, mTimezoneAdapter.getRowById(mTimezone), this); 305 mTimezoneDialog = builder.create(); 306 mTimezoneFooterView.setText( 307 mActivity.getString(R.string.edit_event_show_all) + " >"); 308 mTimezoneFooterView.setOnClickListener(new View.OnClickListener() { 309 @Override 310 public void onClick(View v) { 311 mTimezoneDialog.getListView().removeFooterView(mTimezoneFooterView); 312 mTimezoneAdapter.showAllTimezones(); 313 final int row = mTimezoneAdapter.getRowById(mTimezone); 314 // we need to post the selection changes to have them have 315 // any effect 316 mTimezoneDialog.getListView().post(new Runnable() { 317 @Override 318 public void run() { 319 mTimezoneDialog.getListView().setItemChecked(row, true); 320 mTimezoneDialog.getListView().setSelection(row); 321 } 322 }); 323 } 324 }); 325 mTimezoneDialog.getListView().addFooterView(mTimezoneFooterView); 326 mTimezoneDialog.show(); 327 } 328 329 private void populateRepeats() { 330 Time time = mStartTime; 331 Resources r = mActivity.getResources(); 332 int resource = android.R.layout.simple_spinner_item; 333 334 String[] days = new String[] { 335 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM), 336 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM), 337 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM), 338 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM), 339 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM), 340 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM), 341 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM), 342 }; 343 String[] ordinals = r.getStringArray(R.array.ordinal_labels); 344 345 // Only display "Custom" in the spinner if the device does not support 346 // the recurrence functionality of the event. Only display every weekday 347 // if the event starts on a weekday. 348 boolean isCustomRecurrence = isCustomRecurrence(); 349 boolean isWeekdayEvent = isWeekdayEvent(); 350 351 ArrayList<String> repeatArray = new ArrayList<String>(0); 352 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0); 353 354 repeatArray.add(r.getString(R.string.does_not_repeat)); 355 recurrenceIndexes.add(EditEventHelper.DOES_NOT_REPEAT); 356 357 repeatArray.add(r.getString(R.string.daily)); 358 recurrenceIndexes.add(EditEventHelper.REPEATS_DAILY); 359 360 if (isWeekdayEvent) { 361 repeatArray.add(r.getString(R.string.every_weekday)); 362 recurrenceIndexes.add(EditEventHelper.REPEATS_EVERY_WEEKDAY); 363 } 364 365 String format = r.getString(R.string.weekly); 366 repeatArray.add(String.format(format, time.format("%A"))); 367 recurrenceIndexes.add(EditEventHelper.REPEATS_WEEKLY_ON_DAY); 368 369 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance 370 // of the given day. 371 int dayNumber = (time.monthDay - 1) / 7; 372 format = r.getString(R.string.monthly_on_day_count); 373 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay])); 374 recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT); 375 376 format = r.getString(R.string.monthly_on_day); 377 repeatArray.add(String.format(format, time.monthDay)); 378 recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY); 379 380 long when = time.toMillis(false); 381 format = r.getString(R.string.yearly); 382 int flags = 0; 383 if (DateFormat.is24HourFormat(mActivity)) { 384 flags |= DateUtils.FORMAT_24HOUR; 385 } 386 repeatArray.add(String.format(format, DateUtils.formatDateTime(mActivity, when, flags))); 387 recurrenceIndexes.add(EditEventHelper.REPEATS_YEARLY); 388 389 if (isCustomRecurrence) { 390 repeatArray.add(r.getString(R.string.custom)); 391 recurrenceIndexes.add(EditEventHelper.REPEATS_CUSTOM); 392 } 393 mRecurrenceIndexes = recurrenceIndexes; 394 395 int position = recurrenceIndexes.indexOf(EditEventHelper.DOES_NOT_REPEAT); 396 if (mModel.mRrule != null) { 397 if (isCustomRecurrence) { 398 position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_CUSTOM); 399 } else { 400 switch (mEventRecurrence.freq) { 401 case EventRecurrence.DAILY: 402 position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_DAILY); 403 break; 404 case EventRecurrence.WEEKLY: 405 if (mEventRecurrence.repeatsOnEveryWeekDay()) { 406 position = recurrenceIndexes 407 .indexOf(EditEventHelper.REPEATS_EVERY_WEEKDAY); 408 } else { 409 position = recurrenceIndexes 410 .indexOf(EditEventHelper.REPEATS_WEEKLY_ON_DAY); 411 } 412 break; 413 case EventRecurrence.MONTHLY: 414 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 415 position = recurrenceIndexes 416 .indexOf(EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT); 417 } else { 418 position = recurrenceIndexes 419 .indexOf(EditEventHelper.REPEATS_MONTHLY_ON_DAY); 420 } 421 break; 422 case EventRecurrence.YEARLY: 423 position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_YEARLY); 424 break; 425 } 426 } 427 } 428 ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity, resource, repeatArray); 429 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 430 mRepeatsSpinner.setAdapter(adapter); 431 mRepeatsSpinner.setSelection(position); 432 433 // Don't allow the user to make exceptions recurring events. 434 if (mModel.mOriginalEvent != null) { 435 mRepeatsSpinner.setEnabled(false); 436 } 437 } 438 439 private boolean isCustomRecurrence() { 440 441 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) { 442 return true; 443 } 444 445 if (mEventRecurrence.freq == 0) { 446 return false; 447 } 448 449 switch (mEventRecurrence.freq) { 450 case EventRecurrence.DAILY: 451 return false; 452 case EventRecurrence.WEEKLY: 453 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) { 454 return false; 455 } else if (mEventRecurrence.bydayCount == 1) { 456 return false; 457 } 458 break; 459 case EventRecurrence.MONTHLY: 460 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 461 return false; 462 } else if (mEventRecurrence.bydayCount == 0 463 && mEventRecurrence.bymonthdayCount == 1) { 464 return false; 465 } 466 break; 467 case EventRecurrence.YEARLY: 468 return false; 469 } 470 471 return true; 472 } 473 474 private boolean isWeekdayEvent() { 475 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) { 476 return true; 477 } 478 return false; 479 } 480 481 private class DateClickListener implements View.OnClickListener { 482 private Time mTime; 483 484 public DateClickListener(Time time) { 485 mTime = time; 486 } 487 488 public void onClick(View v) { 489 new DatePickerDialog(mActivity, new DateListener(v), mTime.year, mTime.month, 490 mTime.monthDay).show(); 491 } 492 } 493 494 static private class CalendarsAdapter extends ResourceCursorAdapter { 495 public CalendarsAdapter(Context context, Cursor c) { 496 super(context, R.layout.calendars_item, c); 497 setDropDownViewResource(R.layout.calendars_dropdown_item); 498 } 499 500 @Override 501 public void bindView(View view, Context context, Cursor cursor) { 502 View colorBar = view.findViewById(R.id.color); 503 int colorColumn = cursor.getColumnIndexOrThrow(Calendars.COLOR); 504 int nameColumn = cursor.getColumnIndexOrThrow(Calendars.DISPLAY_NAME); 505 int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT); 506 if (colorBar != null) { 507 colorBar.setBackgroundDrawable(Utils.getColorChip(cursor.getInt(colorColumn))); 508 } 509 510 TextView name = (TextView) view.findViewById(R.id.calendar_name); 511 if (name != null) { 512 String displayName = cursor.getString(nameColumn); 513 name.setText(displayName); 514 name.setTextColor(0xFF000000); 515 516 TextView accountName = (TextView) view.findViewById(R.id.account_name); 517 if (accountName != null) { 518 Resources res = context.getResources(); 519 accountName.setText(cursor.getString(ownerColumn)); 520 accountName.setVisibility(TextView.VISIBLE); 521 accountName.setTextColor(res.getColor(R.color.calendar_owner_text_color)); 522 } 523 } 524 } 525 } 526 527 // This is called if the user clicks on one of the buttons: "Save", 528 // "Discard", or "Delete". This is also called if the user clicks 529 // on the "remove reminder" button. 530 public void onClick(View v) { 531 if (v == mSaveButton) { 532 // If we're creating a new event but haven't gotten any calendars 533 // yet let the 534 // user know we're waiting for calendars to finish loading. The save 535 // button 536 // isn't enabled until we have a non-null mModel. 537 if (mCalendarsCursor == null && mModel.mUri == null) { 538 if (mLoadingCalendarsDialog == null) { 539 // Create the progress dialog 540 mLoadingCalendarsDialog = ProgressDialog.show(mActivity, mActivity 541 .getText(R.string.loading_calendars_title), mActivity 542 .getText(R.string.loading_calendars_message), true, true, this); 543 mSaveAfterQueryComplete = true; 544 } 545 } else if (fillModelFromUI()) { 546 if (!mModel.mAllDay) { 547 mTimezoneAdapter.saveRecentTimezone(mTimezone); 548 } 549 mDone.setDoneCode(Utils.DONE_SAVE); 550 mDone.run(); 551 } else { 552 mDone.setDoneCode(Utils.DONE_REVERT); 553 mDone.run(); 554 } 555 return; 556 } 557 558 if (v == mDeleteButton) { 559 mDone.setDoneCode(Utils.DONE_DELETE); 560 mDone.run(); 561 return; 562 } 563 564 if (v == mDiscardButton) { 565 mDone.setDoneCode(Utils.DONE_REVERT); 566 mDone.run(); 567 return; 568 } 569 570 // This must be a click on one of the "remove reminder" buttons 571 LinearLayout reminderItem = (LinearLayout) v.getParent(); 572 LinearLayout parent = (LinearLayout) reminderItem.getParent(); 573 parent.removeView(reminderItem); 574 mReminderItems.remove(reminderItem); 575 updateRemindersVisibility(mReminderItems.size()); 576 } 577 578 // This is called if the user cancels the "No calendars" dialog. 579 // The "No calendars" dialog is shown if there are no syncable calendars. 580 public void onCancel(DialogInterface dialog) { 581 if (dialog == mLoadingCalendarsDialog) { 582 mLoadingCalendarsDialog = null; 583 mSaveAfterQueryComplete = false; 584 } else if (dialog == mNoCalendarsDialog) { 585 mDone.setDoneCode(Utils.DONE_REVERT); 586 mDone.run(); 587 return; 588 } 589 } 590 591 // This is called if the user clicks on a dialog button. 592 public void onClick(DialogInterface dialog, int which) { 593 if (dialog == mNoCalendarsDialog) { 594 mDone.setDoneCode(Utils.DONE_REVERT); 595 mDone.run(); 596 } else if (dialog == mTimezoneDialog) { 597 if (which >= 0 && which < mTimezoneAdapter.getCount()) { 598 setTimezone(which); 599 dialog.dismiss(); 600 } 601 } 602 } 603 604 // Goes through the UI elements and updates the model as necessary 605 private boolean fillModelFromUI() { 606 if (mModel == null) { 607 return false; 608 } 609 mModel.mReminderMinutes = EventViewUtils.reminderItemsToMinutes(mReminderItems, 610 mReminderValues); 611 mModel.mHasAlarm = mReminderItems.size() > 0; 612 mModel.mTitle = mTitleTextView.getText().toString().trim(); 613 mModel.mAllDay = mAllDayCheckBox.isChecked(); 614 mModel.mLocation = mLocationTextView.getText().toString().trim(); 615 mModel.mDescription = mDescriptionTextView.getText().toString().trim(); 616 int position = mResponseSpinner.getSelectedItemPosition(); 617 if (position > 0) { 618 mModel.mSelfAttendeeStatus = EditEventHelper.ATTENDEE_VALUES[position]; 619 } 620 621 if (mGuestList != null) { 622 AttendeesAdapter adapter = (AttendeesAdapter) mGuestList.getAdapter(); 623 if (adapter != null && !adapter.isEmpty()) { 624 int size = adapter.getCount(); 625 mModel.mAttendeesList.clear(); 626 for (int i = 0; i < size; i++) { 627 Attendee attendee = adapter.getItem(i); 628 if (attendee == null || adapter.isRemoved(i)) { 629 continue; 630 } 631 mModel.addAttendee(attendee); 632 } 633 } 634 } 635 636 // If this was a new event we need to fill in the Calendar information 637 if (mModel.mUri == null) { 638 mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId(); 639 int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition(); 640 if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) { 641 String defaultCalendar = mCalendarsCursor 642 .getString(EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT); 643 Utils.setSharedPreference(mActivity, 644 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, defaultCalendar); 645 mModel.mOwnerAccount = defaultCalendar; 646 mModel.mOrganizer = defaultCalendar; 647 mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID); 648 } 649 } 650 651 if (mModel.mAllDay) { 652 // Reset start and end time, increment the monthDay by 1, and set 653 // the timezone to UTC, as required for all-day events. 654 mTimezone = Time.TIMEZONE_UTC; 655 mStartTime.hour = 0; 656 mStartTime.minute = 0; 657 mStartTime.second = 0; 658 mStartTime.timezone = mTimezone; 659 mModel.mStart = mStartTime.normalize(true); 660 661 // Round up to the next day 662 if (mEndTime.hour > 0 || mEndTime.minute > 0 || mEndTime.second > 0 663 || mEndTime.monthDay == mStartTime.monthDay) { 664 mEndTime.monthDay++; 665 } 666 mEndTime.hour = 0; 667 mEndTime.minute = 0; 668 mEndTime.second = 0; 669 mEndTime.timezone = mTimezone; 670 mModel.mEnd = mEndTime.normalize(true); 671 } else { 672 mStartTime.timezone = mTimezone; 673 mEndTime.timezone = mTimezone; 674 mModel.mStart = mStartTime.toMillis(true); 675 mModel.mEnd = mEndTime.toMillis(true); 676 } 677 mModel.mTimezone = mTimezone; 678 mModel.mVisibility = mVisibilitySpinner.getSelectedItemPosition(); 679 mModel.mTransparency = mTransparencySpinner.getSelectedItemPosition() != 0; 680 681 int selection; 682 // If we're making an exception we don't want it to be a repeating 683 // event. 684 if (mModification == EditEventHelper.MODIFY_SELECTED) { 685 selection = EditEventHelper.DOES_NOT_REPEAT; 686 } else { 687 position = mRepeatsSpinner.getSelectedItemPosition(); 688 selection = mRecurrenceIndexes.get(position); 689 } 690 691 EditEventHelper.updateRecurrenceRule(selection, mModel, 692 Utils.getFirstDayOfWeek(mActivity) + 1); 693 694 return true; 695 } 696 697 public EditEventView(Activity activity, View view, EditDoneRunnable done) { 698 699 mActivity = activity; 700 mView = view; 701 mDone = done; 702 703 // cache top level view elements 704 mLoadingMessage = (TextView) view.findViewById(R.id.loading_message); 705 mScrollView = (ScrollView) view.findViewById(R.id.scroll_view); 706 707 mLayoutInflater = activity.getLayoutInflater(); 708 709 // cache all the widgets 710 mTitleTextView = (TextView) view.findViewById(R.id.title); 711 mLocationTextView = (TextView) view.findViewById(R.id.location); 712 mDescriptionTextView = (TextView) view.findViewById(R.id.description); 713 mTimezoneTextView = (TextView) view.findViewById(R.id.timezone_label); 714 mTimezoneFooterView = (TextView) mLayoutInflater.inflate(R.layout.timezone_footer, null); 715 mStartDateButton = (Button) view.findViewById(R.id.start_date); 716 mEndDateButton = (Button) view.findViewById(R.id.end_date); 717 mStartTimeButton = (Button) view.findViewById(R.id.start_time); 718 mEndTimeButton = (Button) view.findViewById(R.id.end_time); 719 mTimezoneButton = (Button) view.findViewById(R.id.timezone); 720 mAllDayCheckBox = (CheckBox) view.findViewById(R.id.is_all_day); 721 mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars); 722 mRepeatsSpinner = (Spinner) view.findViewById(R.id.repeats); 723 mTransparencySpinner = (Spinner) view.findViewById(R.id.availability); 724 mVisibilitySpinner = (Spinner) view.findViewById(R.id.visibility); 725 mResponseSpinner = (Spinner) view.findViewById(R.id.response_value); 726 mRemindersSeparator = view.findViewById(R.id.reminders_separator); 727 mRemindersContainer = (LinearLayout) view.findViewById(R.id.reminder_items_container); 728 729 mSaveButton = (Button) view.findViewById(R.id.save); 730 mDeleteButton = (Button) view.findViewById(R.id.delete); 731 732 mDiscardButton = (Button) view.findViewById(R.id.discard); 733 mDiscardButton.setOnClickListener(this); 734 735 mAddAttendeesButton = (ImageButton) view.findViewById(R.id.attendee_add); 736 mAddAttendeesButton.setOnClickListener(new AddAttendeeClickListener()); 737 738 mStartTime = new Time(); 739 mEndTime = new Time(); 740 mTimezone = TimeZone.getDefault().getID(); 741 mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone); 742 743 mGuestList = (ListView) mView.findViewById(R.id.attendee_list); 744 745 // Display loading screen 746 setModel(null); 747 } 748 749 /** 750 * Fill in the view with the contents of the given event model. This allows 751 * an edit view to be initialized before the event has been loaded. Passing 752 * in null for the model will display a loading screen. A non-null model 753 * will fill in the view's fields with the data contained in the model. 754 * 755 * @param model The event model to pull the data from 756 */ 757 public void setModel(CalendarEventModel model) { 758 mModel = model; 759 if (model == null) { 760 // Display loading screen 761 mLoadingMessage.setVisibility(View.VISIBLE); 762 mScrollView.setVisibility(View.GONE); 763 mSaveButton.setEnabled(false); 764 mDeleteButton.setEnabled(false); 765 return; 766 } 767 768 long begin = model.mStart; 769 long end = model.mEnd; 770 mTimezone = model.mTimezone; // this will be UTC for all day events 771 772 // Set up the starting times 773 if (begin > 0) { 774 mStartTime.timezone = mTimezone; 775 mStartTime.set(begin); 776 mStartTime.normalize(true); 777 } 778 if (end > 0) { 779 mEndTime.timezone = mTimezone; 780 mEndTime.set(end); 781 mEndTime.normalize(true); 782 } 783 String rrule = model.mRrule; 784 if (rrule != null) { 785 mEventRecurrence.parse(rrule); 786 } 787 788 // If the user is allowed to change the attendees set up the view and 789 // validator 790 if (model.mHasAttendeeData) { 791 String domain = "gmail.com"; 792 if (!TextUtils.isEmpty(model.mOwnerAccount)) { 793 String ownerDomain = EditEventHelper.extractDomain(model.mOwnerAccount); 794 if (!TextUtils.isEmpty(ownerDomain)) { 795 domain = ownerDomain; 796 } 797 } 798 mAddressAdapter = new EmailAddressAdapter(mActivity); 799 mEmailValidator = new Rfc822Validator(domain); 800 mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees); 801 } else { 802 View attGroup = mView.findViewById(R.id.attendees_group); 803 attGroup.setVisibility(View.GONE); 804 } 805 806 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 807 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 808 if (isChecked) { 809 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 810 mEndTime.monthDay--; 811 long endMillis = mEndTime.normalize(true); 812 813 // Do not allow an event to have an end time before the 814 // start time. 815 if (mEndTime.before(mStartTime)) { 816 mEndTime.set(mStartTime); 817 endMillis = mEndTime.normalize(true); 818 } 819 setDate(mEndDateButton, endMillis); 820 setTime(mEndTimeButton, endMillis); 821 } 822 823 mStartTimeButton.setVisibility(View.GONE); 824 mEndTimeButton.setVisibility(View.GONE); 825 mTimezoneButton.setVisibility(View.GONE); 826 mTimezoneTextView.setVisibility(View.GONE); 827 } else { 828 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 829 mEndTime.monthDay++; 830 long endMillis = mEndTime.normalize(true); 831 setDate(mEndDateButton, endMillis); 832 setTime(mEndTimeButton, endMillis); 833 } 834 mStartTimeButton.setVisibility(View.VISIBLE); 835 mEndTimeButton.setVisibility(View.VISIBLE); 836 mTimezoneButton.setVisibility(View.VISIBLE); 837 mTimezoneTextView.setVisibility(View.VISIBLE); 838 } 839 } 840 }); 841 842 if (model.mAllDay) { 843 mAllDayCheckBox.setChecked(true); 844 // put things back in local time for all day events 845 mTimezone = TimeZone.getDefault().getID(); 846 mStartTime.timezone = mTimezone; 847 mStartTime.normalize(true); 848 mEndTime.timezone = mTimezone; 849 mEndTime.normalize(true); 850 } else { 851 mAllDayCheckBox.setChecked(false); 852 } 853 854 mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone); 855 if (mTimezoneDialog != null) { 856 mTimezoneDialog.getListView().setAdapter(mTimezoneAdapter); 857 } 858 859 mSaveButton.setOnClickListener(this); 860 mDeleteButton.setOnClickListener(this); 861 mSaveButton.setEnabled(true); 862 mDeleteButton.setEnabled(true); 863 864 // Initialize the reminder values array. 865 Resources r = mActivity.getResources(); 866 String[] strings = r.getStringArray(R.array.reminder_minutes_values); 867 int size = strings.length; 868 ArrayList<Integer> list = new ArrayList<Integer>(size); 869 for (int i = 0; i < size; i++) { 870 list.add(Integer.parseInt(strings[i])); 871 } 872 mReminderValues = list; 873 String[] labels = r.getStringArray(R.array.reminder_minutes_labels); 874 mReminderLabels = new ArrayList<String>(Arrays.asList(labels)); 875 876 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(mActivity); 877 String durationString = prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, 878 "0"); 879 mDefaultReminderMinutes = Integer.parseInt(durationString); 880 881 int numReminders = 0; 882 if (model.mHasAlarm) { 883 ArrayList<Integer> minutes = model.mReminderMinutes; 884 numReminders = minutes.size(); 885 for (Integer minute : minutes) { 886 EventViewUtils.addMinutesToList( mActivity, mReminderValues, mReminderLabels, 887 minute); 888 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, 889 mReminderValues, mReminderLabels, minute); 890 } 891 } 892 updateRemindersVisibility(numReminders); 893 894 // Setup the + Add Reminder Button 895 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { 896 public void onClick(View v) { 897 addReminder(); 898 } 899 }; 900 ImageButton reminderRemoveButton = (ImageButton) mView.findViewById(R.id.reminder_add); 901 reminderRemoveButton.setOnClickListener(addReminderOnClickListener); 902 903 mTitleTextView.setText(model.mTitle); 904 mLocationTextView.setText(model.mLocation); 905 mDescriptionTextView.setText(model.mDescription); 906 mTransparencySpinner.setSelection(model.mTransparency ? 1 : 0); 907 mVisibilitySpinner.setSelection(model.mVisibility); 908 mResponseSpinner.setSelection(findResponseIndexFor(model.mSelfAttendeeStatus)); 909 910 if (model.mUri != null) { 911 // This is an existing event so hide the calendar spinner 912 // since we can't change the calendar. 913 View calendarGroup = mView.findViewById(R.id.calendar_group); 914 calendarGroup.setVisibility(View.GONE); 915 } else { 916 mDeleteButton.setVisibility(View.GONE); 917 } 918 919 populateWhen(); 920 populateTimezone(); 921 populateRepeats(); 922 updateAttendees(model.mAttendeesList); 923 mScrollView.setVisibility(View.VISIBLE); 924 mLoadingMessage.setVisibility(View.GONE); 925 } 926 927 private int findResponseIndexFor(int response) { 928 int size = EditEventHelper.ATTENDEE_VALUES.length; 929 for (int index = 0; index < size; index++) { 930 if (EditEventHelper.ATTENDEE_VALUES[index] == response) { 931 return index; 932 } 933 } 934 return 0; 935 } 936 937 public void setCalendarsCursor(Cursor cursor) { 938 // If there are no syncable calendars, then we cannot allow 939 // creating a new event. 940 mCalendarsCursor = cursor; 941 if (cursor == null || cursor.getCount() == 0) { 942 // Cancel the "loading calendars" dialog if it exists 943 if (mSaveAfterQueryComplete) { 944 mLoadingCalendarsDialog.cancel(); 945 } 946 // Create an error message for the user that, when clicked, 947 // will exit this activity without saving the event. 948 AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); 949 builder.setTitle(R.string.no_syncable_calendars).setIcon( 950 android.R.drawable.ic_dialog_alert).setMessage(R.string.no_calendars_found) 951 .setPositiveButton(android.R.string.ok, this).setOnCancelListener(this); 952 mNoCalendarsDialog = builder.show(); 953 return; 954 } 955 956 int defaultCalendarPosition = findDefaultCalendarPosition(cursor); 957 958 // populate the calendars spinner 959 CalendarsAdapter adapter = new CalendarsAdapter(mActivity, cursor); 960 mCalendarsSpinner.setAdapter(adapter); 961 mCalendarsSpinner.setSelection(defaultCalendarPosition); 962 mCalendarsCursorSet = true; 963 964 // Find user domain and set it to the validator. 965 // TODO: we may want to update this validator if the user actually picks 966 // a different calendar. maybe not. depends on what we want for the 967 // user experience. this may change when we add support for multiple 968 // accounts, anyway. 969 if (mModel != null && mModel.mHasAttendeeData 970 && cursor.moveToPosition(defaultCalendarPosition)) { 971 String ownEmail = cursor.getString(EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT); 972 if (ownEmail != null) { 973 String domain = EditEventHelper.extractDomain(ownEmail); 974 if (domain != null) { 975 mEmailValidator = new Rfc822Validator(domain); 976 mAttendeesList.setValidator(mEmailValidator); 977 } 978 } 979 } 980 if (mSaveAfterQueryComplete) { 981 mLoadingCalendarsDialog.cancel(); 982 if (fillModelFromUI()) { 983 mDone.setDoneCode(Utils.DONE_SAVE); 984 mDone.run(); 985 } else { 986 mDone.setDoneCode(Utils.DONE_REVERT); 987 mDone.run(); 988 } 989 return; 990 } 991 } 992 993 public void setModification(int modifyWhich) { 994 mModification = modifyWhich; 995 // If we are modifying all the events in a 996 // series then disable and ignore the date. 997 if (modifyWhich == Utils.MODIFY_ALL) { 998 mStartDateButton.setEnabled(false); 999 mEndDateButton.setEnabled(false); 1000 } else if (modifyWhich == Utils.MODIFY_SELECTED) { 1001 mRepeatsSpinner.setEnabled(false); 1002 } 1003 } 1004 1005 // Find the calendar position in the cursor that matches calendar in 1006 // preference 1007 private int findDefaultCalendarPosition(Cursor calendarsCursor) { 1008 if (calendarsCursor.getCount() <= 0) { 1009 return -1; 1010 } 1011 1012 String defaultCalendar = Utils.getSharedPreference(mActivity, 1013 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, null); 1014 1015 if (defaultCalendar == null) { 1016 return 0; 1017 } 1018 int calendarsOwnerColumn = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT); 1019 int position = 0; 1020 calendarsCursor.moveToPosition(-1); 1021 while (calendarsCursor.moveToNext()) { 1022 if (defaultCalendar.equals(calendarsCursor.getString(calendarsOwnerColumn))) { 1023 return position; 1024 } 1025 position++; 1026 } 1027 return 0; 1028 } 1029 1030 public void updateAttendees(HashMap<String, Attendee> attendeesList) { 1031 if (mAttendeesAdapter == null) { 1032 mAttendeesAdapter = new AttendeesAdapter(mActivity, mEmailValidator); 1033 } 1034 if (attendeesList.size() > 0) { 1035 mAttendeesAdapter.addAttendees(attendeesList); 1036 mGuestList.setAdapter(mAttendeesAdapter); 1037 } 1038 } 1039 1040 private void updateRemindersVisibility(int numReminders) { 1041 if (numReminders == 0) { 1042 mRemindersSeparator.setVisibility(View.GONE); 1043 mRemindersContainer.setVisibility(View.GONE); 1044 } else { 1045 mRemindersSeparator.setVisibility(View.VISIBLE); 1046 mRemindersContainer.setVisibility(View.VISIBLE); 1047 } 1048 } 1049 1050 public void addReminder() { 1051 // TODO: when adding a new reminder, make it different from the 1052 // last one in the list (if any). 1053 if (mDefaultReminderMinutes == 0) { 1054 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, 1055 mReminderValues, mReminderLabels, 10 /* minutes */); 1056 } else { 1057 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, 1058 mReminderValues, mReminderLabels, mDefaultReminderMinutes); 1059 } 1060 updateRemindersVisibility(mReminderItems.size()); 1061 mScrollView.fling(REMINDER_FLING_VELOCITY); 1062 } 1063 1064 public int getReminderCount() { 1065 return mReminderItems.size(); 1066 } 1067 1068 // From com.google.android.gm.ComposeActivity 1069 private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) { 1070 MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) mView.findViewById(res); 1071 list.setAdapter(mAddressAdapter); 1072 list.setTokenizer(new Rfc822Tokenizer()); 1073 list.setValidator(mEmailValidator); 1074 1075 // NOTE: assumes no other filters are set 1076 list.setFilters(sRecipientFilters); 1077 1078 return list; 1079 } 1080 1081 /** 1082 * From com.google.android.gm.ComposeActivity Implements special address 1083 * cleanup rules: The first space key entry following an "@" symbol that is 1084 * followed by any combination of letters and symbols, including one+ dots 1085 * and zero commas, should insert an extra comma (followed by the space). 1086 */ 1087 private static InputFilter[] sRecipientFilters = new InputFilter[] { 1088 new Rfc822InputFilter() 1089 }; 1090 1091 private void setDate(TextView view, long millis) { 1092 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR 1093 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH 1094 | DateUtils.FORMAT_ABBREV_WEEKDAY; 1095 1096 // Unfortunately, DateUtils doesn't support a timezone other than the 1097 // default timezone provided by the system, so we have this ugly hack 1098 // here to trick it into formatting our time correctly. In order to 1099 // prevent all sorts of craziness, we synchronize on the TimeZone class 1100 // to prevent other threads from reading an incorrect timezone from 1101 // calls to TimeZone#getDefault() 1102 // TODO fix this if/when DateUtils allows for passing in a timezone 1103 String dateString; 1104 synchronized (TimeZone.class) { 1105 TimeZone.setDefault(TimeZone.getTimeZone(mTimezone)); 1106 dateString = DateUtils.formatDateTime(mActivity, millis, flags); 1107 // setting the default back to null restores the correct behavior 1108 TimeZone.setDefault(null); 1109 } 1110 view.setText(dateString); 1111 } 1112 1113 private void setTime(TextView view, long millis) { 1114 int flags = DateUtils.FORMAT_SHOW_TIME; 1115 if (DateFormat.is24HourFormat(mActivity)) { 1116 flags |= DateUtils.FORMAT_24HOUR; 1117 } 1118 1119 // Unfortunately, DateUtils doesn't support a timezone other than the 1120 // default timezone provided by the system, so we have this ugly hack 1121 // here to trick it into formatting our time correctly. In order to 1122 // prevent all sorts of craziness, we synchronize on the TimeZone class 1123 // to prevent other threads from reading an incorrect timezone from 1124 // calls to TimeZone#getDefault() 1125 // TODO fix this if/when DateUtils allows for passing in a timezone 1126 String timeString; 1127 synchronized (TimeZone.class) { 1128 TimeZone.setDefault(TimeZone.getTimeZone(mTimezone)); 1129 timeString = DateUtils.formatDateTime(mActivity, millis, flags); 1130 TimeZone.setDefault(null); 1131 } 1132 view.setText(timeString); 1133 } 1134 1135 private void setTimezone(int i) { 1136 if (i < 0 || i >= mTimezoneAdapter.getCount()) { 1137 return; // do nothing 1138 } 1139 TimezoneRow timezone = mTimezoneAdapter.getItem(i); 1140 mTimezoneButton.setText(timezone.toString()); 1141 mTimezone = timezone.mId; 1142 mStartTime.timezone = mTimezone; 1143 mStartTime.normalize(true); 1144 mEndTime.timezone = mTimezone; 1145 mEndTime.normalize(true); 1146 mTimezoneAdapter.setCurrentTimezone(mTimezone); 1147 } 1148} 1149