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