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