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