EditEventView.java revision 016d576c1f0fbcbf060fab132d8e6a1016dd7091
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.CalendarEventModel.ReminderEntry; 22import com.android.calendar.EmailAddressAdapter; 23import com.android.calendar.EventInfoFragment; 24import com.android.calendar.GeneralPreferences; 25import com.android.calendar.R; 26import com.android.calendar.TimezoneAdapter; 27import com.android.calendar.TimezoneAdapter.TimezoneRow; 28import com.android.calendar.Utils; 29import com.android.calendar.event.EditEventHelper.EditDoneRunnable; 30import com.android.common.Rfc822InputFilter; 31import com.android.common.Rfc822Validator; 32 33import android.app.Activity; 34import android.app.AlertDialog; 35import android.app.DatePickerDialog; 36import android.app.DatePickerDialog.OnDateSetListener; 37import android.app.ProgressDialog; 38import android.app.TimePickerDialog; 39import android.app.TimePickerDialog.OnTimeSetListener; 40import android.content.Context; 41import android.content.DialogInterface; 42import android.content.Intent; 43import android.content.SharedPreferences; 44import android.content.res.Resources; 45import android.database.Cursor; 46import android.graphics.drawable.Drawable; 47import android.pim.EventRecurrence; 48import android.provider.Calendar.Attendees; 49import android.provider.Calendar.Calendars; 50import android.provider.Calendar.Reminders; 51import android.provider.Settings; 52import android.text.Editable; 53import android.text.InputFilter; 54import android.text.TextUtils; 55import android.text.TextWatcher; 56import android.text.format.DateFormat; 57import android.text.format.DateUtils; 58import android.text.format.Time; 59import android.text.util.Rfc822Tokenizer; 60import android.util.Log; 61import android.view.LayoutInflater; 62import android.view.View; 63import android.view.ViewGroup; 64import android.view.accessibility.AccessibilityEvent; 65import android.view.accessibility.AccessibilityManager; 66import android.widget.AdapterView; 67import android.widget.AdapterView.OnItemSelectedListener; 68import android.widget.ArrayAdapter; 69import android.widget.Button; 70import android.widget.CalendarView; 71import android.widget.CheckBox; 72import android.widget.CompoundButton; 73import android.widget.DatePicker; 74import android.widget.ImageButton; 75import android.widget.LinearLayout; 76import android.widget.MultiAutoCompleteTextView; 77import android.widget.RadioButton; 78import android.widget.RadioGroup; 79import android.widget.ResourceCursorAdapter; 80import android.widget.ScrollView; 81import android.widget.Spinner; 82import android.widget.TextView; 83import android.widget.TimePicker; 84 85import java.util.ArrayList; 86import java.util.Arrays; 87import java.util.Calendar; 88import java.util.Formatter; 89import java.util.HashMap; 90import java.util.Locale; 91import java.util.TimeZone; 92 93public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener, 94 DialogInterface.OnClickListener, TextWatcher, OnItemSelectedListener { 95 private static final String TAG = "EditEvent"; 96 private static final String GOOGLE_SECONDARY_CALENDAR = "calendar.google.com"; 97 private static final String PERIOD_SPACE = ". "; 98 99 private static String DEFAULT_DOMAIN; 100 101 ArrayList<View> mEditOnlyList = new ArrayList<View>(); 102 ArrayList<View> mEditViewList = new ArrayList<View>(); 103 ArrayList<View> mViewOnlyList = new ArrayList<View>(); 104 TextView mLoadingMessage; 105 ScrollView mScrollView; 106 Button mStartDateButton; 107 Button mEndDateButton; 108 Button mStartTimeButton; 109 Button mEndTimeButton; 110 Button mTimezoneButton; 111 TextView mStartTimeHome; 112 TextView mStartDateHome; 113 TextView mEndTimeHome; 114 TextView mEndDateHome; 115 CheckBox mAllDayCheckBox; 116 Spinner mCalendarsSpinner; 117 Spinner mRepeatsSpinner; 118 Spinner mAvailabilitySpinner; 119 Spinner mAccessLevelSpinner; 120 RadioGroup mResponseRadioGroup; 121 TextView mTitleTextView; 122 TextView mLocationTextView; 123 TextView mDescriptionTextView; 124 TextView mWhenView; 125 TextView mTimezoneTextView; 126 TextView mTimezoneLabel; 127 LinearLayout mRemindersContainer; 128 MultiAutoCompleteTextView mAttendeesList; 129 ImageButton mAddAttendeesButton; 130 AttendeesView mAttendeesView; 131 AddAttendeeClickListener mAddAttendeesListener; 132 View mCalendarSelectorGroup; 133 View mCalendarStaticGroup; 134 View mLocationGroup; 135 View mDescriptionGroup; 136 View mRemindersGroup; 137 View mResponseGroup; 138 View mOrganizerGroup; 139 View mAttendeesGroup; 140 View mStartHomeGroup; 141 View mEndHomeGroup; 142 View mAttendeesPane; 143 View mColorChip; 144 145 private int[] mOriginalPadding = new int[4]; 146 147 private ProgressDialog mLoadingCalendarsDialog; 148 private AlertDialog mNoCalendarsDialog; 149 private AlertDialog mTimezoneDialog; 150 private Activity mActivity; 151 private EditDoneRunnable mDone; 152 private View mView; 153 private CalendarEventModel mModel; 154 private Cursor mCalendarsCursor; 155 private EmailAddressAdapter mAddressAdapter; 156 private Rfc822Validator mEmailValidator; 157 private TimezoneAdapter mTimezoneAdapter; 158 159 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer>(0); 160 161 /** 162 * Contents of the "minutes" spinner. This has default values from the XML file, augmented 163 * with any additional values that were already associated with the event. 164 */ 165 private ArrayList<Integer> mReminderMinuteValues; 166 private ArrayList<String> mReminderMinuteLabels; 167 168 /** 169 * Contents of the "methods" spinner. The "values" list specifies the method constant 170 * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels. Any methods that 171 * aren't allowed by the Calendar will be removed. 172 */ 173 private ArrayList<Integer> mReminderMethodValues; 174 private ArrayList<String> mReminderMethodLabels; 175 176 private int mDefaultReminderMinutes; 177 178 private boolean mSaveAfterQueryComplete = false; 179 180 private Time mStartTime; 181 private Time mEndTime; 182 private String mTimezone; 183 private int mModification = EditEventHelper.MODIFY_UNINITIALIZED; 184 185 private EventRecurrence mEventRecurrence = new EventRecurrence(); 186 187 private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0); 188 189 private static StringBuilder mSB = new StringBuilder(50); 190 private static Formatter mF = new Formatter(mSB, Locale.getDefault()); 191 192 /* This class is used to update the time buttons. */ 193 private class TimeListener implements OnTimeSetListener { 194 private View mView; 195 196 public TimeListener(View view) { 197 mView = view; 198 } 199 200 @Override 201 public void onTimeSet(TimePicker view, int hourOfDay, int minute) { 202 // Cache the member variables locally to avoid inner class overhead. 203 Time startTime = mStartTime; 204 Time endTime = mEndTime; 205 206 // Cache the start and end millis so that we limit the number 207 // of calls to normalize() and toMillis(), which are fairly 208 // expensive. 209 long startMillis; 210 long endMillis; 211 if (mView == mStartTimeButton) { 212 // The start time was changed. 213 int hourDuration = endTime.hour - startTime.hour; 214 int minuteDuration = endTime.minute - startTime.minute; 215 216 startTime.hour = hourOfDay; 217 startTime.minute = minute; 218 startMillis = startTime.normalize(true); 219 220 // Also update the end time to keep the duration constant. 221 endTime.hour = hourOfDay + hourDuration; 222 endTime.minute = minute + minuteDuration; 223 } else { 224 // The end time was changed. 225 startMillis = startTime.toMillis(true); 226 endTime.hour = hourOfDay; 227 endTime.minute = minute; 228 229 // Move to the start time if the end time is before the start 230 // time. 231 if (endTime.before(startTime)) { 232 endTime.monthDay = startTime.monthDay + 1; 233 } 234 } 235 236 endMillis = endTime.normalize(true); 237 238 setDate(mEndDateButton, endMillis); 239 setTime(mStartTimeButton, startMillis); 240 setTime(mEndTimeButton, endMillis); 241 updateHomeTime(); 242 } 243 } 244 245 private class AddAttendeeClickListener implements View.OnClickListener { 246 @Override 247 public void onClick(View v) { 248 // Checking for null since this method may be called even though the 249 // add button wasn't clicked e.g. when the Save button is clicked. 250 // The mAttendeesList may not be setup since the user doesn't have 251 // permission to add attendees. 252 if (mAttendeesList != null) { 253 mAttendeesList.performValidation(); 254 mAttendeesView.addAttendees(mAttendeesList.getText().toString()); 255 mAttendeesList.setText(""); 256 mAttendeesGroup.setVisibility(View.VISIBLE); 257 } 258 } 259 } 260 261 private class TimeClickListener implements View.OnClickListener { 262 private Time mTime; 263 264 public TimeClickListener(Time time) { 265 mTime = time; 266 } 267 268 @Override 269 public void onClick(View v) { 270 new TimePickerDialog(mActivity, new TimeListener(v), mTime.hour, mTime.minute, 271 DateFormat.is24HourFormat(mActivity)).show(); 272 } 273 } 274 275 private class DateListener implements OnDateSetListener { 276 View mView; 277 278 public DateListener(View view) { 279 mView = view; 280 } 281 282 @Override 283 public void onDateSet(DatePicker view, int year, int month, int monthDay) { 284 Log.d(TAG, "onDateSet: " + year + " " + month + " " + monthDay); 285 // Cache the member variables locally to avoid inner class overhead. 286 Time startTime = mStartTime; 287 Time endTime = mEndTime; 288 289 // Cache the start and end millis so that we limit the number 290 // of calls to normalize() and toMillis(), which are fairly 291 // expensive. 292 long startMillis; 293 long endMillis; 294 if (mView == mStartDateButton) { 295 // The start date was changed. 296 int yearDuration = endTime.year - startTime.year; 297 int monthDuration = endTime.month - startTime.month; 298 int monthDayDuration = endTime.monthDay - startTime.monthDay; 299 300 startTime.year = year; 301 startTime.month = month; 302 startTime.monthDay = monthDay; 303 startMillis = startTime.normalize(true); 304 305 // Also update the end date to keep the duration constant. 306 endTime.year = year + yearDuration; 307 endTime.month = month + monthDuration; 308 endTime.monthDay = monthDay + monthDayDuration; 309 endMillis = endTime.normalize(true); 310 311 // If the start date has changed then update the repeats. 312 populateRepeats(); 313 } else { 314 // The end date was changed. 315 startMillis = startTime.toMillis(true); 316 endTime.year = year; 317 endTime.month = month; 318 endTime.monthDay = monthDay; 319 endMillis = endTime.normalize(true); 320 321 // Do not allow an event to have an end time before the start 322 // time. 323 if (endTime.before(startTime)) { 324 endTime.set(startTime); 325 endMillis = startMillis; 326 } 327 } 328 329 setDate(mStartDateButton, startMillis); 330 setDate(mEndDateButton, endMillis); 331 setTime(mEndTimeButton, endMillis); // In case end time had to be 332 // reset 333 updateHomeTime(); 334 } 335 } 336 337 // Fills in the date and time fields 338 private void populateWhen() { 339 long startMillis = mStartTime.toMillis(false /* use isDst */); 340 long endMillis = mEndTime.toMillis(false /* use isDst */); 341 setDate(mStartDateButton, startMillis); 342 setDate(mEndDateButton, endMillis); 343 344 setTime(mStartTimeButton, startMillis); 345 setTime(mEndTimeButton, endMillis); 346 347 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime)); 348 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime)); 349 350 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime)); 351 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime)); 352 } 353 354 private void populateTimezone() { 355 mTimezoneButton.setOnClickListener(new View.OnClickListener() { 356 @Override 357 public void onClick(View v) { 358 showTimezoneDialog(); 359 } 360 }); 361 setTimezone(mTimezoneAdapter.getRowById(mTimezone)); 362 } 363 364 private void showTimezoneDialog() { 365 AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); 366 final Context alertDialogContext = builder.getContext(); 367 mTimezoneAdapter = new TimezoneAdapter(alertDialogContext, mTimezone); 368 builder.setTitle(R.string.timezone_label); 369 builder.setSingleChoiceItems( 370 mTimezoneAdapter, mTimezoneAdapter.getRowById(mTimezone), this); 371 mTimezoneDialog = builder.create(); 372 373 LayoutInflater layoutInflater = (LayoutInflater) alertDialogContext 374 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 375 final TextView timezoneFooterView = (TextView) layoutInflater.inflate( 376 R.layout.timezone_footer, null); 377 378 timezoneFooterView.setText(mActivity.getString(R.string.edit_event_show_all) + " >"); 379 timezoneFooterView.setOnClickListener(new View.OnClickListener() { 380 @Override 381 public void onClick(View v) { 382 mTimezoneDialog.getListView().removeFooterView(timezoneFooterView); 383 mTimezoneAdapter.showAllTimezones(); 384 final int row = mTimezoneAdapter.getRowById(mTimezone); 385 // we need to post the selection changes to have them have 386 // any effect 387 mTimezoneDialog.getListView().post(new Runnable() { 388 @Override 389 public void run() { 390 mTimezoneDialog.getListView().setItemChecked(row, true); 391 mTimezoneDialog.getListView().setSelection(row); 392 } 393 }); 394 } 395 }); 396 mTimezoneDialog.getListView().addFooterView(timezoneFooterView); 397 mTimezoneDialog.show(); 398 } 399 400 private void populateRepeats() { 401 Time time = mStartTime; 402 Resources r = mActivity.getResources(); 403 int resource = android.R.layout.simple_spinner_item; 404 405 String[] days = new String[] { 406 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM), 407 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM), 408 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM), 409 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM), 410 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM), 411 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM), 412 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM), }; 413 String[] ordinals = r.getStringArray(R.array.ordinal_labels); 414 415 // Only display "Custom" in the spinner if the device does not support 416 // the recurrence functionality of the event. Only display every weekday 417 // if the event starts on a weekday. 418 boolean isCustomRecurrence = isCustomRecurrence(); 419 boolean isWeekdayEvent = isWeekdayEvent(); 420 421 ArrayList<String> repeatArray = new ArrayList<String>(0); 422 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0); 423 424 repeatArray.add(r.getString(R.string.does_not_repeat)); 425 recurrenceIndexes.add(EditEventHelper.DOES_NOT_REPEAT); 426 427 repeatArray.add(r.getString(R.string.daily)); 428 recurrenceIndexes.add(EditEventHelper.REPEATS_DAILY); 429 430 if (isWeekdayEvent) { 431 repeatArray.add(r.getString(R.string.every_weekday)); 432 recurrenceIndexes.add(EditEventHelper.REPEATS_EVERY_WEEKDAY); 433 } 434 435 String format = r.getString(R.string.weekly); 436 repeatArray.add(String.format(format, time.format("%A"))); 437 recurrenceIndexes.add(EditEventHelper.REPEATS_WEEKLY_ON_DAY); 438 439 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance 440 // of the given day. 441 int dayNumber = (time.monthDay - 1) / 7; 442 format = r.getString(R.string.monthly_on_day_count); 443 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay])); 444 recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT); 445 446 format = r.getString(R.string.monthly_on_day); 447 repeatArray.add(String.format(format, time.monthDay)); 448 recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY); 449 450 long when = time.toMillis(false); 451 format = r.getString(R.string.yearly); 452 int flags = 0; 453 if (DateFormat.is24HourFormat(mActivity)) { 454 flags |= DateUtils.FORMAT_24HOUR; 455 } 456 repeatArray.add(String.format(format, DateUtils.formatDateTime(mActivity, when, flags))); 457 recurrenceIndexes.add(EditEventHelper.REPEATS_YEARLY); 458 459 if (isCustomRecurrence) { 460 repeatArray.add(r.getString(R.string.custom)); 461 recurrenceIndexes.add(EditEventHelper.REPEATS_CUSTOM); 462 } 463 mRecurrenceIndexes = recurrenceIndexes; 464 465 int position = recurrenceIndexes.indexOf(EditEventHelper.DOES_NOT_REPEAT); 466 if (!TextUtils.isEmpty(mModel.mRrule)) { 467 if (isCustomRecurrence) { 468 position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_CUSTOM); 469 } else { 470 switch (mEventRecurrence.freq) { 471 case EventRecurrence.DAILY: 472 position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_DAILY); 473 break; 474 case EventRecurrence.WEEKLY: 475 if (mEventRecurrence.repeatsOnEveryWeekDay()) { 476 position = recurrenceIndexes.indexOf( 477 EditEventHelper.REPEATS_EVERY_WEEKDAY); 478 } else { 479 position = recurrenceIndexes.indexOf( 480 EditEventHelper.REPEATS_WEEKLY_ON_DAY); 481 } 482 break; 483 case EventRecurrence.MONTHLY: 484 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 485 position = recurrenceIndexes.indexOf( 486 EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT); 487 } else { 488 position = recurrenceIndexes.indexOf( 489 EditEventHelper.REPEATS_MONTHLY_ON_DAY); 490 } 491 break; 492 case EventRecurrence.YEARLY: 493 position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_YEARLY); 494 break; 495 } 496 } 497 } 498 ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity, resource, repeatArray); 499 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 500 mRepeatsSpinner.setAdapter(adapter); 501 mRepeatsSpinner.setSelection(position); 502 503 // Don't allow the user to make exceptions recurring events. 504 if (mModel.mOriginalEvent != null) { 505 mRepeatsSpinner.setEnabled(false); 506 } 507 } 508 509 private boolean isCustomRecurrence() { 510 511 if (mEventRecurrence.until != null 512 || (mEventRecurrence.interval != 0 && mEventRecurrence.interval != 1)) { 513 return true; 514 } 515 516 if (mEventRecurrence.freq == 0) { 517 return false; 518 } 519 520 switch (mEventRecurrence.freq) { 521 case EventRecurrence.DAILY: 522 return false; 523 case EventRecurrence.WEEKLY: 524 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) { 525 return false; 526 } else if (mEventRecurrence.bydayCount == 1) { 527 return false; 528 } 529 break; 530 case EventRecurrence.MONTHLY: 531 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 532 return false; 533 } else if (mEventRecurrence.bydayCount == 0 534 && mEventRecurrence.bymonthdayCount == 1) { 535 return false; 536 } 537 break; 538 case EventRecurrence.YEARLY: 539 return false; 540 } 541 542 return true; 543 } 544 545 private boolean isWeekdayEvent() { 546 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) { 547 return true; 548 } 549 return false; 550 } 551 552 private class DateClickListener implements View.OnClickListener { 553 private Time mTime; 554 555 public DateClickListener(Time time) { 556 mTime = time; 557 } 558 559 public void onClick(View v) { 560 DatePickerDialog dpd = new DatePickerDialog( 561 mActivity, new DateListener(v), mTime.year, mTime.month, mTime.monthDay); 562 CalendarView cv = dpd.getDatePicker().getCalendarView(); 563 cv.setShowWeekNumber(Utils.getShowWeekNumber(mActivity)); 564 int startOfWeek = Utils.getFirstDayOfWeek(mActivity); 565 // Utils returns Time days while CalendarView wants Calendar days 566 if (startOfWeek == Time.SATURDAY) { 567 startOfWeek = Calendar.SATURDAY; 568 } else if (startOfWeek == Time.SUNDAY) { 569 startOfWeek = Calendar.SUNDAY; 570 } else { 571 startOfWeek = Calendar.MONDAY; 572 } 573 cv.setFirstDayOfWeek(startOfWeek); 574 dpd.show(); 575 } 576 } 577 578 static private class CalendarsAdapter extends ResourceCursorAdapter { 579 public CalendarsAdapter(Context context, Cursor c) { 580 super(context, R.layout.calendars_item, c); 581 setDropDownViewResource(R.layout.calendars_dropdown_item); 582 } 583 584 @Override 585 public void bindView(View view, Context context, Cursor cursor) { 586 View colorBar = view.findViewById(R.id.color); 587 int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR); 588 int nameColumn = cursor.getColumnIndexOrThrow(Calendars.DISPLAY_NAME); 589 int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT); 590 if (colorBar != null) { 591 colorBar.setBackgroundColor(cursor.getInt(colorColumn)); 592 } 593 594 TextView name = (TextView) view.findViewById(R.id.calendar_name); 595 if (name != null) { 596 String displayName = cursor.getString(nameColumn); 597 name.setText(displayName); 598 599 TextView accountName = (TextView) view.findViewById(R.id.account_name); 600 if (accountName != null) { 601 Resources res = context.getResources(); 602 accountName.setText(cursor.getString(ownerColumn)); 603 accountName.setVisibility(TextView.VISIBLE); 604 accountName.setTextColor(res.getColor(R.color.calendar_owner_text_color)); 605 } 606 } 607 } 608 } 609 610 /** 611 * Does prep steps for saving a calendar event. 612 * 613 * This triggers a parse of the attendees list and checks if the event is 614 * ready to be saved. An event is ready to be saved so long as a model 615 * exists and has a calendar it can be associated with, either because it's 616 * an existing event or we've finished querying. 617 * 618 * @return false if there is no model or no calendar had been loaded yet, 619 * true otherwise. 620 */ 621 public boolean prepareForSave() { 622 if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) { 623 return false; 624 } 625 mAddAttendeesListener.onClick(null); 626 return fillModelFromUI(); 627 } 628 629 public boolean fillModelFromReadOnlyUi() { 630 if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) { 631 return false; 632 } 633 mModel.mReminders = EventViewUtils.reminderItemsToReminders( 634 mReminderItems, mReminderMinuteValues, mReminderMethodValues); 635 int status = EventInfoFragment.getResponseFromButtonId( 636 mResponseRadioGroup.getCheckedRadioButtonId()); 637 if (status != Attendees.ATTENDEE_STATUS_NONE) { 638 mModel.mSelfAttendeeStatus = status; 639 } 640 return true; 641 } 642 643 // This is called if the user clicks on one of the buttons: "Save", 644 // "Discard", or "Delete". This is also called if the user clicks 645 // on the "remove reminder" button. 646 @Override 647 public void onClick(View view) { 648 649 // This must be a click on one of the "remove reminder" buttons 650 LinearLayout reminderItem = (LinearLayout) view.getParent(); 651 LinearLayout parent = (LinearLayout) reminderItem.getParent(); 652 parent.removeView(reminderItem); 653 mReminderItems.remove(reminderItem); 654 updateRemindersVisibility(mReminderItems.size()); 655 } 656 657 // This is called if the user cancels the "No calendars" dialog. 658 // The "No calendars" dialog is shown if there are no syncable calendars. 659 @Override 660 public void onCancel(DialogInterface dialog) { 661 if (dialog == mLoadingCalendarsDialog) { 662 mLoadingCalendarsDialog = null; 663 mSaveAfterQueryComplete = false; 664 } else if (dialog == mNoCalendarsDialog) { 665 mDone.setDoneCode(Utils.DONE_REVERT); 666 mDone.run(); 667 return; 668 } 669 } 670 671 // This is called if the user clicks on a dialog button. 672 @Override 673 public void onClick(DialogInterface dialog, int which) { 674 if (dialog == mNoCalendarsDialog) { 675 mDone.setDoneCode(Utils.DONE_REVERT); 676 mDone.run(); 677 if (which == DialogInterface.BUTTON_POSITIVE) { 678 Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT); 679 final String[] array = {"com.android.calendar"}; 680 nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array); 681 nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 682 mActivity.startActivity(nextIntent); 683 } 684 } else if (dialog == mTimezoneDialog) { 685 if (which >= 0 && which < mTimezoneAdapter.getCount()) { 686 setTimezone(which); 687 updateHomeTime(); 688 dialog.dismiss(); 689 } 690 } 691 } 692 693 // Goes through the UI elements and updates the model as necessary 694 private boolean fillModelFromUI() { 695 if (mModel == null) { 696 return false; 697 } 698 mModel.mReminders = EventViewUtils.reminderItemsToReminders(mReminderItems, 699 mReminderMinuteValues, mReminderMethodValues); 700 mModel.mHasAlarm = mReminderItems.size() > 0; 701 mModel.mTitle = mTitleTextView.getText().toString(); 702 mModel.mAllDay = mAllDayCheckBox.isChecked(); 703 mModel.mLocation = mLocationTextView.getText().toString(); 704 mModel.mDescription = mDescriptionTextView.getText().toString(); 705 if (TextUtils.isEmpty(mModel.mLocation)) { 706 mModel.mLocation = null; 707 } 708 if (TextUtils.isEmpty(mModel.mDescription)) { 709 mModel.mDescription = null; 710 } 711 712 int status = EventInfoFragment.getResponseFromButtonId(mResponseRadioGroup 713 .getCheckedRadioButtonId()); 714 if (status != Attendees.ATTENDEE_STATUS_NONE) { 715 mModel.mSelfAttendeeStatus = status; 716 } 717 718 if (mAttendeesView != null && mAttendeesView.getChildCount() > 0) { 719 final int size = mAttendeesView.getChildCount(); 720 mModel.mAttendeesList.clear(); 721 for (int i = 0; i < size; i++) { 722 final Attendee attendee = mAttendeesView.getItem(i); 723 if (attendee == null || mAttendeesView.isMarkAsRemoved(i)) { 724 continue; 725 } 726 mModel.addAttendee(attendee); 727 } 728 } 729 730 // If this was a new event we need to fill in the Calendar information 731 if (mModel.mUri == null) { 732 mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId(); 733 int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition(); 734 if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) { 735 String defaultCalendar = mCalendarsCursor.getString( 736 EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT); 737 Utils.setSharedPreference( 738 mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, defaultCalendar); 739 mModel.mOwnerAccount = defaultCalendar; 740 mModel.mOrganizer = defaultCalendar; 741 mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID); 742 } 743 } 744 745 if (mModel.mAllDay) { 746 // Reset start and end time, increment the monthDay by 1, and set 747 // the timezone to UTC, as required for all-day events. 748 mTimezone = Time.TIMEZONE_UTC; 749 mStartTime.hour = 0; 750 mStartTime.minute = 0; 751 mStartTime.second = 0; 752 mStartTime.timezone = mTimezone; 753 mModel.mStart = mStartTime.normalize(true); 754 755 mEndTime.hour = 0; 756 mEndTime.minute = 0; 757 mEndTime.second = 0; 758 mEndTime.timezone = mTimezone; 759 // When a user see the event duration as "X - Y" (e.g. Oct. 28 - Oct. 29), end time 760 // should be Y + 1 (Oct.30). 761 final long normalizedEndTimeMillis = 762 mEndTime.normalize(true) + DateUtils.DAY_IN_MILLIS; 763 if (normalizedEndTimeMillis < mModel.mStart) { 764 // mEnd should be midnight of the next day of mStart. 765 mModel.mEnd = mModel.mStart + DateUtils.DAY_IN_MILLIS; 766 } else { 767 mModel.mEnd = normalizedEndTimeMillis; 768 } 769 } else { 770 mStartTime.timezone = mTimezone; 771 mEndTime.timezone = mTimezone; 772 mModel.mStart = mStartTime.toMillis(true); 773 mModel.mEnd = mEndTime.toMillis(true); 774 } 775 mModel.mTimezone = mTimezone; 776 mModel.mAccessLevel = mAccessLevelSpinner.getSelectedItemPosition(); 777 mModel.mAvailability = mAvailabilitySpinner.getSelectedItemPosition() != 0; 778 779 int selection; 780 // If we're making an exception we don't want it to be a repeating 781 // event. 782 if (mModification == EditEventHelper.MODIFY_SELECTED) { 783 selection = EditEventHelper.DOES_NOT_REPEAT; 784 } else { 785 int position = mRepeatsSpinner.getSelectedItemPosition(); 786 selection = mRecurrenceIndexes.get(position); 787 } 788 789 EditEventHelper.updateRecurrenceRule( 790 selection, mModel, Utils.getFirstDayOfWeek(mActivity) + 1); 791 792 // Save the timezone so we can display it as a standard option next time 793 if (!mModel.mAllDay) { 794 mTimezoneAdapter.saveRecentTimezone(mTimezone); 795 } 796 return true; 797 } 798 799 public EditEventView(Activity activity, View view, EditDoneRunnable done) { 800 801 mActivity = activity; 802 mView = view; 803 mDone = done; 804 805 DEFAULT_DOMAIN = activity.getResources().getString(R.string.google_email_domain); 806 807 // cache top level view elements 808 mLoadingMessage = (TextView) view.findViewById(R.id.loading_message); 809 mScrollView = (ScrollView) view.findViewById(R.id.scroll_view); 810 811 // cache all the widgets 812 mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars_spinner); 813 mTitleTextView = (TextView) view.findViewById(R.id.title); 814 mLocationTextView = (TextView) view.findViewById(R.id.location); 815 mDescriptionTextView = (TextView) view.findViewById(R.id.description); 816 mTimezoneLabel = (TextView) view.findViewById(R.id.timezone_label); 817 mStartDateButton = (Button) view.findViewById(R.id.start_date); 818 mEndDateButton = (Button) view.findViewById(R.id.end_date); 819 mWhenView = (TextView) mView.findViewById(R.id.when); 820 mTimezoneTextView = (TextView) mView.findViewById(R.id.timezone_textView); 821 mStartTimeButton = (Button) view.findViewById(R.id.start_time); 822 mEndTimeButton = (Button) view.findViewById(R.id.end_time); 823 mTimezoneButton = (Button) view.findViewById(R.id.timezone_button); 824 mStartTimeHome = (TextView) view.findViewById(R.id.start_time_home_tz); 825 mStartDateHome = (TextView) view.findViewById(R.id.start_date_home_tz); 826 mEndTimeHome = (TextView) view.findViewById(R.id.end_time_home_tz); 827 mEndDateHome = (TextView) view.findViewById(R.id.end_date_home_tz); 828 mAllDayCheckBox = (CheckBox) view.findViewById(R.id.is_all_day); 829 mRepeatsSpinner = (Spinner) view.findViewById(R.id.repeats); 830 mAvailabilitySpinner = (Spinner) view.findViewById(R.id.availability); 831 mAccessLevelSpinner = (Spinner) view.findViewById(R.id.visibility); 832 mCalendarSelectorGroup = view.findViewById(R.id.calendar_selector_group); 833 mCalendarStaticGroup = view.findViewById(R.id.calendar_group); 834 mRemindersGroup = view.findViewById(R.id.reminders_row); 835 mResponseGroup = view.findViewById(R.id.response_row); 836 mOrganizerGroup = view.findViewById(R.id.organizer_row); 837 mAttendeesGroup = view.findViewById(R.id.attendees_row); 838 mAttendeesPane = view.findViewById(R.id.attendees_group); 839 mLocationGroup = view.findViewById(R.id.where_row); 840 mDescriptionGroup = view.findViewById(R.id.description_row); 841 mStartHomeGroup = view.findViewById(R.id.from_row_home_tz); 842 mEndHomeGroup = view.findViewById(R.id.to_row_home_tz); 843 844 mTitleTextView.setTag(mTitleTextView.getBackground()); 845 mLocationTextView.setTag(mLocationTextView.getBackground()); 846 mDescriptionTextView.setTag(mDescriptionTextView.getBackground()); 847 mRepeatsSpinner.setTag(mRepeatsSpinner.getBackground()); 848 mOriginalPadding[0] = mLocationTextView.getPaddingLeft(); 849 mOriginalPadding[1] = mLocationTextView.getPaddingTop(); 850 mOriginalPadding[2] = mLocationTextView.getPaddingRight(); 851 mOriginalPadding[3] = mLocationTextView.getPaddingBottom(); 852 mEditViewList.add(mTitleTextView); 853 mEditViewList.add(mLocationTextView); 854 mEditViewList.add(mDescriptionTextView); 855 856 mViewOnlyList.add(view.findViewById(R.id.when_row)); 857 mViewOnlyList.add(view.findViewById(R.id.timezone_textview_row)); 858 859 mEditOnlyList.add(view.findViewById(R.id.all_day_row)); 860 mEditOnlyList.add(view.findViewById(R.id.availability_row)); 861 mEditOnlyList.add(view.findViewById(R.id.visibility_row)); 862 mEditOnlyList.add(view.findViewById(R.id.from_row)); 863 mEditOnlyList.add(view.findViewById(R.id.to_row)); 864 mEditOnlyList.add(view.findViewById(R.id.timezone_button_row)); 865 mEditOnlyList.add(view.findViewById(R.id.add_attendees_row)); 866 mEditOnlyList.add(mStartHomeGroup); 867 mEditOnlyList.add(mEndHomeGroup); 868 869 mResponseRadioGroup = (RadioGroup) view.findViewById(R.id.response_value); 870 mRemindersContainer = (LinearLayout) view.findViewById(R.id.reminder_items_container); 871 872 mAddAttendeesButton = (ImageButton) view.findViewById(R.id.add_attendee_button); 873 mAddAttendeesListener = new AddAttendeeClickListener(); 874 mAddAttendeesButton.setEnabled(false); 875 mAddAttendeesButton.setOnClickListener(mAddAttendeesListener); 876 877 mAttendeesView = (AttendeesView)view.findViewById(R.id.attendee_list); 878 879 mTimezone = Utils.getTimeZone(activity, null); 880 mStartTime = new Time(mTimezone); 881 mEndTime = new Time(mTimezone); 882 mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone); 883 884 mColorChip = view.findViewById(R.id.color_chip); 885 886 // Display loading screen 887 setModel(null); 888 } 889 890 891 /** 892 * Loads an integer array asset into a list. 893 */ 894 private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) { 895 int[] vals = r.getIntArray(resNum); 896 int size = vals.length; 897 ArrayList<Integer> list = new ArrayList<Integer>(size); 898 899 for (int i = 0; i < size; i++) { 900 list.add(vals[i]); 901 } 902 903 return list; 904 } 905 906 /** 907 * Loads a String array asset into a list. 908 */ 909 private static ArrayList<String> loadStringArray(Resources r, int resNum) { 910 String[] labels = r.getStringArray(resNum); 911 ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels)); 912 return list; 913 } 914 915 /** 916 * Prepares the reminder UI elements. 917 * <p> 918 * (Re-)loads the minutes / methods lists from the XML assets, adds/removes items as 919 * needed for the current set of reminders and calendar properties, and then creates UI 920 * elements. 921 */ 922 private void prepareReminders() { 923 CalendarEventModel model = mModel; 924 Resources r = mActivity.getResources(); 925 926 // Load the labels and corresponding numeric values for the minutes and methods lists 927 // from the assets. If we're switching calendars, we need to clear and re-populate the 928 // lists (which may have elements added and removed based on calendar properties). This 929 // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a 930 // new event that aren't in the default set. 931 mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values); 932 mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels); 933 mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values); 934 mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels); 935 936 // Remove any reminder methods that aren't allowed for this calendar. If this is 937 // a new event, mCalendarAllowedReminders may not be set the first time we're called. 938 if (mModel.mCalendarAllowedReminders != null) { 939 EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels, 940 mModel.mCalendarAllowedReminders); 941 } 942 943 int numReminders = 0; 944 if (model.mHasAlarm) { 945 ArrayList<ReminderEntry> reminders = model.mReminders; 946 numReminders = reminders.size(); 947 // Insert any minute values that aren't represented in the minutes list. 948 for (ReminderEntry re : reminders) { 949 EventViewUtils.addMinutesToList( 950 mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes()); 951 } 952 953 // Create a UI element for each reminder. We display all of the reminders we get 954 // from the provider, even if the count exceeds the calendar maximum. (Also, for 955 // a new event, we won't have a maxReminders value available.) 956 for (ReminderEntry re : reminders) { 957 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, 958 mReminderMinuteValues, mReminderMinuteLabels, 959 mReminderMethodValues, mReminderMethodLabels, 960 re, Integer.MAX_VALUE); 961 } 962 } 963 964 updateRemindersVisibility(numReminders); 965 } 966 967 /** 968 * Fill in the view with the contents of the given event model. This allows 969 * an edit view to be initialized before the event has been loaded. Passing 970 * in null for the model will display a loading screen. A non-null model 971 * will fill in the view's fields with the data contained in the model. 972 * 973 * @param model The event model to pull the data from 974 */ 975 public void setModel(CalendarEventModel model) { 976 mModel = model; 977 978 // Need to close the autocomplete adapter to prevent leaking cursors. 979 if (mAddressAdapter != null) { 980 mAddressAdapter.close(); 981 mAddressAdapter = null; 982 } 983 984 if (model == null) { 985 // Display loading screen 986 mLoadingMessage.setVisibility(View.VISIBLE); 987 mScrollView.setVisibility(View.GONE); 988 return; 989 } 990 991 boolean canModifyCalendar = EditEventHelper.canModifyCalendar(model); 992 boolean canModifyEvent = EditEventHelper.canModifyEvent(model); 993 boolean canRespond = EditEventHelper.canRespond(model); 994 995 long begin = model.mStart; 996 long end = model.mEnd; 997 mTimezone = model.mTimezone; // this will be UTC for all day events 998 999 // Set up the starting times 1000 if (begin > 0) { 1001 mStartTime.timezone = mTimezone; 1002 mStartTime.set(begin); 1003 mStartTime.normalize(true); 1004 } 1005 if (end > 0) { 1006 mEndTime.timezone = mTimezone; 1007 mEndTime.set(end); 1008 mEndTime.normalize(true); 1009 } 1010 String rrule = model.mRrule; 1011 if (!TextUtils.isEmpty(rrule)) { 1012 mEventRecurrence.parse(rrule); 1013 } 1014 1015 // If the user is allowed to change the attendees set up the view and 1016 // validator 1017 if (!model.mHasAttendeeData) { 1018 mView.findViewById(R.id.attendees_group).setVisibility(View.GONE); 1019 } else if (!canModifyEvent) { 1020 // Hide views used for adding attendees 1021 View v = mView.findViewById(R.id.add_attendees_label); 1022 if (v != null) { 1023 v.setVisibility(View.GONE); 1024 } 1025 mView.findViewById(R.id.add_attendees_group).setVisibility(View.GONE); 1026 mAddAttendeesButton.setVisibility(View.GONE); 1027 } else { 1028 String domain = DEFAULT_DOMAIN; 1029 if (!TextUtils.isEmpty(model.mOwnerAccount)) { 1030 String ownerDomain = EditEventHelper.extractDomain(model.mOwnerAccount); 1031 if (!TextUtils.isEmpty(ownerDomain)) { 1032 domain = ownerDomain; 1033 } 1034 } 1035 mAddressAdapter = new EmailAddressAdapter(mActivity); 1036 mEmailValidator = new Rfc822Validator(domain); 1037 mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees); 1038 mAttendeesList.addTextChangedListener(this); 1039 } 1040 1041 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 1042 @Override 1043 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 1044 setAllDayViewsVisibility(isChecked); 1045 } 1046 }); 1047 1048 if (model.mAllDay) { 1049 mAllDayCheckBox.setChecked(true); 1050 // put things back in local time for all day events 1051 mTimezone = TimeZone.getDefault().getID(); 1052 mStartTime.timezone = mTimezone; 1053 mStartTime.normalize(true); 1054 mEndTime.timezone = mTimezone; 1055 mEndTime.normalize(true); 1056 } else { 1057 mAllDayCheckBox.setChecked(false); 1058 } 1059 1060 mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone); 1061 if (mTimezoneDialog != null) { 1062 mTimezoneDialog.getListView().setAdapter(mTimezoneAdapter); 1063 } 1064 1065 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity); 1066 String defaultReminderString = prefs.getString( 1067 GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING); 1068 mDefaultReminderMinutes = Integer.parseInt(defaultReminderString); 1069 1070 prepareReminders(); 1071 1072 ImageButton reminderAddButton = (ImageButton) mView.findViewById(R.id.reminder_add); 1073 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { 1074 @Override 1075 public void onClick(View v) { 1076 addReminder(); 1077 } 1078 }; 1079 reminderAddButton.setOnClickListener(addReminderOnClickListener); 1080 1081 mTitleTextView.setText(model.mTitle); 1082 if (model.mIsOrganizer || TextUtils.isEmpty(model.mOrganizer) 1083 || model.mOrganizer.endsWith(GOOGLE_SECONDARY_CALENDAR)) { 1084 mView.findViewById(R.id.organizer_label).setVisibility(View.GONE); 1085 mView.findViewById(R.id.organizer).setVisibility(View.GONE); 1086 } else { 1087 ((TextView) mView.findViewById(R.id.organizer)).setText(model.mOrganizerDisplayName); 1088 } 1089 mLocationTextView.setText(model.mLocation); 1090 mDescriptionTextView.setText(model.mDescription); 1091 mAvailabilitySpinner.setSelection(model.mAvailability ? 1 : 0); 1092 mAccessLevelSpinner.setSelection(model.mAccessLevel); 1093 1094 View responseLabel = mView.findViewById(R.id.response_label); 1095 if (canRespond) { 1096 int buttonToCheck = EventInfoFragment 1097 .findButtonIdForResponse(model.mSelfAttendeeStatus); 1098 mResponseRadioGroup.check(buttonToCheck); // -1 clear all radio buttons 1099 mResponseRadioGroup.setVisibility(View.VISIBLE); 1100 responseLabel.setVisibility(View.VISIBLE); 1101 } else { 1102 responseLabel.setVisibility(View.GONE); 1103 mResponseRadioGroup.setVisibility(View.GONE); 1104 } 1105 1106 if (model.mUri != null) { 1107 // This is an existing event so hide the calendar spinner 1108 // since we can't change the calendar. 1109 View calendarGroup = mView.findViewById(R.id.calendar_selector_group); 1110 calendarGroup.setVisibility(View.GONE); 1111 TextView tv = (TextView) mView.findViewById(R.id.calendar_textview); 1112 tv.setText(model.mCalendarDisplayName); 1113 mColorChip.setBackgroundColor(model.mCalendarColor); 1114 } else { 1115 View calendarGroup = mView.findViewById(R.id.calendar_group); 1116 calendarGroup.setVisibility(View.GONE); 1117 mCalendarsSpinner.setBackgroundColor(model.mCalendarColor); 1118 mCalendarSelectorGroup.setBackgroundColor(model.mCalendarColor); 1119 } 1120 1121 populateTimezone(); 1122 populateWhen(); 1123 populateRepeats(); 1124 updateAttendees(model.mAttendeesList); 1125 1126 updateView(); 1127 mScrollView.setVisibility(View.VISIBLE); 1128 mLoadingMessage.setVisibility(View.GONE); 1129 sendAccessibilityEvent(); 1130 } 1131 1132 private void sendAccessibilityEvent() { 1133 AccessibilityManager am = AccessibilityManager.getInstance(mActivity); 1134 if (!am.isEnabled() || mModel == null) { 1135 return; 1136 } 1137 StringBuilder b = new StringBuilder(); 1138 addFieldsRecursive(b, mView); 1139 CharSequence msg = b.toString(); 1140 1141 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 1142 event.setClassName(getClass().getName()); 1143 event.setPackageName(mActivity.getPackageName()); 1144 event.getText().add(msg); 1145 event.setAddedCount(msg.length()); 1146 1147 am.sendAccessibilityEvent(event); 1148 } 1149 1150 private void addFieldsRecursive(StringBuilder b, View v) { 1151 if (v == null || v.getVisibility() != View.VISIBLE) { 1152 return; 1153 } 1154 if (v instanceof TextView) { 1155 CharSequence tv = ((TextView) v).getText(); 1156 if (!TextUtils.isEmpty(tv.toString().trim())) { 1157 b.append(tv + PERIOD_SPACE); 1158 } 1159 } else if (v instanceof RadioGroup) { 1160 RadioGroup rg = (RadioGroup) v; 1161 int id = rg.getCheckedRadioButtonId(); 1162 if (id != View.NO_ID) { 1163 b.append(((RadioButton) (v.findViewById(id))).getText() + PERIOD_SPACE); 1164 } 1165 } else if (v instanceof Spinner) { 1166 Spinner s = (Spinner) v; 1167 if (s.getSelectedItem() instanceof String) { 1168 String str = ((String) (s.getSelectedItem())).trim(); 1169 if (!TextUtils.isEmpty(str)) { 1170 b.append(str + PERIOD_SPACE); 1171 } 1172 } 1173 } else if (v instanceof ViewGroup) { 1174 ViewGroup vg = (ViewGroup) v; 1175 int children = vg.getChildCount(); 1176 for (int i = 0; i < children; i++) { 1177 addFieldsRecursive(b, vg.getChildAt(i)); 1178 } 1179 } 1180 } 1181 1182 /** 1183 * Creates a single line string for the time/duration 1184 */ 1185 protected void setWhenString() { 1186 String when; 1187 int flags = DateUtils.FORMAT_SHOW_DATE; 1188 String tz = mTimezone; 1189 if (mModel.mAllDay) { 1190 flags |= DateUtils.FORMAT_SHOW_WEEKDAY; 1191 tz = Time.TIMEZONE_UTC; 1192 } else { 1193 flags |= DateUtils.FORMAT_SHOW_TIME; 1194 if (DateFormat.is24HourFormat(mActivity)) { 1195 flags |= DateUtils.FORMAT_24HOUR; 1196 } 1197 } 1198 long startMillis = mStartTime.normalize(true); 1199 long endMillis = mEndTime.normalize(true); 1200 mSB.setLength(0); 1201 when = DateUtils 1202 .formatDateRange(mActivity, mF, startMillis, endMillis, flags, tz).toString(); 1203 mWhenView.setText(when); 1204 } 1205 1206 /** 1207 * Configures the Calendars spinner. This is only done for new events, because only new 1208 * events allow you to select a calendar while editing an event. 1209 * <p> 1210 * We tuck a reference to a Cursor with calendar database data into the spinner, so that 1211 * we can easily extract calendar-specific values when the value changes (the spinner's 1212 * onItemSelected callback is configured). 1213 */ 1214 public void setCalendarsCursor(Cursor cursor, boolean userVisible) { 1215 // If there are no syncable calendars, then we cannot allow 1216 // creating a new event. 1217 mCalendarsCursor = cursor; 1218 if (cursor == null || cursor.getCount() == 0) { 1219 // Cancel the "loading calendars" dialog if it exists 1220 if (mSaveAfterQueryComplete) { 1221 mLoadingCalendarsDialog.cancel(); 1222 } 1223 if (!userVisible) { 1224 return; 1225 } 1226 // Create an error message for the user that, when clicked, 1227 // will exit this activity without saving the event. 1228 AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); 1229 builder.setTitle(R.string.no_syncable_calendars).setIconAttribute( 1230 android.R.attr.alertDialogIcon).setMessage(R.string.no_calendars_found) 1231 .setPositiveButton(R.string.add_account, this) 1232 .setNegativeButton(android.R.string.no, this).setOnCancelListener(this); 1233 mNoCalendarsDialog = builder.show(); 1234 return; 1235 } 1236 1237 int defaultCalendarPosition = findDefaultCalendarPosition(cursor); 1238 1239 // populate the calendars spinner 1240 CalendarsAdapter adapter = new CalendarsAdapter(mActivity, cursor); 1241 mCalendarsSpinner.setAdapter(adapter); 1242 mCalendarsSpinner.setSelection(defaultCalendarPosition); 1243 mCalendarsSpinner.setOnItemSelectedListener(this); 1244 1245 int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR); 1246 mColorChip.setBackgroundColor(cursor.getInt(colorColumn)); 1247 1248 1249 // Find user domain and set it to the validator. 1250 // TODO: we may want to update this validator if the user actually picks 1251 // a different calendar. maybe not. depends on what we want for the 1252 // user experience. this may change when we add support for multiple 1253 // accounts, anyway. 1254 if (mModel != null && mModel.mHasAttendeeData 1255 && cursor.moveToPosition(defaultCalendarPosition)) { 1256 String ownEmail = cursor.getString(EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT); 1257 if (ownEmail != null) { 1258 String domain = EditEventHelper.extractDomain(ownEmail); 1259 if (domain != null) { 1260 mEmailValidator = new Rfc822Validator(domain); 1261 mAttendeesList.setValidator(mEmailValidator); 1262 } 1263 } 1264 } 1265 if (mSaveAfterQueryComplete) { 1266 mLoadingCalendarsDialog.cancel(); 1267 if (prepareForSave() && fillModelFromUI()) { 1268 int exit = userVisible ? Utils.DONE_EXIT : 0; 1269 mDone.setDoneCode(Utils.DONE_SAVE | exit); 1270 mDone.run(); 1271 } else if (userVisible) { 1272 mDone.setDoneCode(Utils.DONE_EXIT); 1273 mDone.run(); 1274 } else if (Log.isLoggable(TAG, Log.DEBUG)) { 1275 Log.d(TAG, "SetCalendarsCursor:Save failed and unable to exit view"); 1276 } 1277 return; 1278 } 1279 } 1280 1281 /** 1282 * Updates the view based on {@link #mModification} and {@link #mModel} 1283 */ 1284 public void updateView() { 1285 if (mModel == null) { 1286 return; 1287 } 1288 if (EditEventHelper.canModifyEvent(mModel)) { 1289 setViewStates(mModification); 1290 } else { 1291 setViewStates(Utils.MODIFY_UNINITIALIZED); 1292 } 1293 } 1294 1295 private void setViewStates(int mode) { 1296 // Extra canModify check just in case 1297 if (mode == Utils.MODIFY_UNINITIALIZED || !EditEventHelper.canModifyEvent(mModel)) { 1298 setWhenString(); 1299 1300 for (View v : mViewOnlyList) { 1301 v.setVisibility(View.VISIBLE); 1302 } 1303 for (View v : mEditOnlyList) { 1304 v.setVisibility(View.GONE); 1305 } 1306 for (View v : mEditViewList) { 1307 v.setEnabled(false); 1308 v.setBackgroundDrawable(null); 1309 } 1310 mCalendarSelectorGroup.setVisibility(View.GONE); 1311 mCalendarStaticGroup.setVisibility(View.VISIBLE); 1312 mRepeatsSpinner.setEnabled(false); 1313 mRepeatsSpinner.setBackgroundDrawable(null); 1314 if (EditEventHelper.canAddReminders(mModel)) { 1315 mRemindersGroup.setVisibility(View.VISIBLE); 1316 } else { 1317 mRemindersGroup.setVisibility(View.GONE); 1318 } 1319 setAttendeesEditable(false); 1320 if (mAttendeesView.getChildCount() == 0) { 1321 mAttendeesPane.setVisibility(View.GONE); 1322 } else { 1323 mAttendeesPane.setVisibility(View.VISIBLE); 1324 } 1325 if (mAllDayCheckBox.isChecked()) { 1326 mView.findViewById(R.id.timezone_textview_row).setVisibility(View.GONE); 1327 } 1328 if (TextUtils.isEmpty(mLocationTextView.getText())) { 1329 mLocationGroup.setVisibility(View.GONE); 1330 } 1331 if (TextUtils.isEmpty(mDescriptionTextView.getText())) { 1332 mDescriptionGroup.setVisibility(View.GONE); 1333 } 1334 } else { 1335 for (View v : mViewOnlyList) { 1336 v.setVisibility(View.GONE); 1337 } 1338 for (View v : mEditOnlyList) { 1339 v.setVisibility(View.VISIBLE); 1340 } 1341 for (View v : mEditViewList) { 1342 v.setEnabled(true); 1343 if (v.getTag() != null) { 1344 v.setBackgroundDrawable((Drawable) v.getTag()); 1345 } 1346 } 1347 if (mModel.mUri == null) { 1348 mCalendarSelectorGroup.setVisibility(View.VISIBLE); 1349 mCalendarStaticGroup.setVisibility(View.GONE); 1350 } else { 1351 mCalendarSelectorGroup.setVisibility(View.GONE); 1352 mCalendarStaticGroup.setVisibility(View.VISIBLE); 1353 } 1354 mRepeatsSpinner.setBackgroundDrawable((Drawable) mRepeatsSpinner.getTag()); 1355 if (mModel.mOriginalEvent == null) { 1356 mRepeatsSpinner.setEnabled(true); 1357 } else { 1358 mRepeatsSpinner.setEnabled(false); 1359 } 1360 mRemindersGroup.setVisibility(View.VISIBLE); 1361 setAttendeesEditable(true); 1362 mAttendeesPane.setVisibility(View.VISIBLE); 1363 1364 mLocationGroup.setVisibility(View.VISIBLE); 1365 mDescriptionGroup.setVisibility(View.VISIBLE); 1366 } 1367 } 1368 1369 /** 1370 * Shows or hides the Guests view and sets the buttons for removing 1371 * attendees based on the value passed in. 1372 * 1373 * @param editable View.GONE or View.VISIBLE 1374 */ 1375 protected void setAttendeesEditable(boolean editable) { 1376 int attCount = mAttendeesView.getChildCount(); 1377 if (attCount > 0) { 1378 mResponseGroup.setVisibility(View.VISIBLE); 1379 mAttendeesGroup.setVisibility(View.VISIBLE); 1380 } else { 1381 mResponseGroup.setVisibility(View.GONE); 1382 mAttendeesGroup.setVisibility(View.GONE); 1383 } 1384 mAttendeesView.setEnabled(editable); 1385 } 1386 1387 public void setModification(int modifyWhich) { 1388 mModification = modifyWhich; 1389 updateView(); 1390 updateHomeTime(); 1391 } 1392 1393 // Find the calendar position in the cursor that matches calendar in 1394 // preference 1395 private int findDefaultCalendarPosition(Cursor calendarsCursor) { 1396 if (calendarsCursor.getCount() <= 0) { 1397 return -1; 1398 } 1399 1400 String defaultCalendar = Utils.getSharedPreference( 1401 mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, null); 1402 1403 if (defaultCalendar == null) { 1404 return 0; 1405 } 1406 int calendarsOwnerColumn = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT); 1407 int position = 0; 1408 calendarsCursor.moveToPosition(-1); 1409 while (calendarsCursor.moveToNext()) { 1410 if (defaultCalendar.equals(calendarsCursor.getString(calendarsOwnerColumn))) { 1411 return position; 1412 } 1413 position++; 1414 } 1415 return 0; 1416 } 1417 1418 private void updateAttendees(HashMap<String, Attendee> attendeesList) { 1419 mAttendeesView.setRfc822Validator(mEmailValidator); 1420 mAttendeesView.addAttendees(attendeesList); 1421 } 1422 1423 private void updateRemindersVisibility(int numReminders) { 1424 if (numReminders == 0) { 1425 mRemindersContainer.setVisibility(View.GONE); 1426 } else { 1427 mRemindersContainer.setVisibility(View.VISIBLE); 1428 } 1429 } 1430 1431 /** 1432 * Add a new reminder when the user hits the "add reminder" button. We use the default 1433 * reminder time and method. 1434 */ 1435 private void addReminder() { 1436 // TODO: when adding a new reminder, make it different from the 1437 // last one in the list (if any). 1438 if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) { 1439 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, 1440 mReminderMinuteValues, mReminderMinuteLabels, 1441 mReminderMethodValues, mReminderMethodLabels, 1442 ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), 1443 mModel.mCalendarMaxReminders); 1444 } else { 1445 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, 1446 mReminderMinuteValues, mReminderMinuteLabels, 1447 mReminderMethodValues, mReminderMethodLabels, 1448 ReminderEntry.valueOf(mDefaultReminderMinutes), 1449 mModel.mCalendarMaxReminders); 1450 } 1451 updateRemindersVisibility(mReminderItems.size()); 1452 } 1453 1454 // From com.google.android.gm.ComposeActivity 1455 private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) { 1456 MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) mView.findViewById(res); 1457 list.setAdapter(mAddressAdapter); 1458 list.setTokenizer(new Rfc822Tokenizer()); 1459 list.setValidator(mEmailValidator); 1460 1461 // NOTE: assumes no other filters are set 1462 list.setFilters(sRecipientFilters); 1463 1464 return list; 1465 } 1466 1467 /** 1468 * From com.google.android.gm.ComposeActivity Implements special address 1469 * cleanup rules: The first space key entry following an "@" symbol that is 1470 * followed by any combination of letters and symbols, including one+ dots 1471 * and zero commas, should insert an extra comma (followed by the space). 1472 */ 1473 private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() }; 1474 1475 private void setDate(TextView view, long millis) { 1476 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR 1477 | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH 1478 | DateUtils.FORMAT_ABBREV_WEEKDAY; 1479 1480 // Unfortunately, DateUtils doesn't support a timezone other than the 1481 // default timezone provided by the system, so we have this ugly hack 1482 // here to trick it into formatting our time correctly. In order to 1483 // prevent all sorts of craziness, we synchronize on the TimeZone class 1484 // to prevent other threads from reading an incorrect timezone from 1485 // calls to TimeZone#getDefault() 1486 // TODO fix this if/when DateUtils allows for passing in a timezone 1487 String dateString; 1488 synchronized (TimeZone.class) { 1489 TimeZone.setDefault(TimeZone.getTimeZone(mTimezone)); 1490 dateString = DateUtils.formatDateTime(mActivity, millis, flags); 1491 // setting the default back to null restores the correct behavior 1492 TimeZone.setDefault(null); 1493 } 1494 view.setText(dateString); 1495 } 1496 1497 private void setTime(TextView view, long millis) { 1498 int flags = DateUtils.FORMAT_SHOW_TIME; 1499 if (DateFormat.is24HourFormat(mActivity)) { 1500 flags |= DateUtils.FORMAT_24HOUR; 1501 } 1502 1503 // Unfortunately, DateUtils doesn't support a timezone other than the 1504 // default timezone provided by the system, so we have this ugly hack 1505 // here to trick it into formatting our time correctly. In order to 1506 // prevent all sorts of craziness, we synchronize on the TimeZone class 1507 // to prevent other threads from reading an incorrect timezone from 1508 // calls to TimeZone#getDefault() 1509 // TODO fix this if/when DateUtils allows for passing in a timezone 1510 String timeString; 1511 synchronized (TimeZone.class) { 1512 TimeZone.setDefault(TimeZone.getTimeZone(mTimezone)); 1513 timeString = DateUtils.formatDateTime(mActivity, millis, flags); 1514 TimeZone.setDefault(null); 1515 } 1516 view.setText(timeString); 1517 } 1518 1519 private void setTimezone(int i) { 1520 if (i < 0 || i >= mTimezoneAdapter.getCount()) { 1521 return; // do nothing 1522 } 1523 TimezoneRow timezone = mTimezoneAdapter.getItem(i); 1524 mTimezoneTextView.setText(timezone.toString()); 1525 mTimezoneButton.setText(timezone.toString()); 1526 mTimezone = timezone.mId; 1527 mStartTime.timezone = mTimezone; 1528 mStartTime.normalize(true); 1529 mEndTime.timezone = mTimezone; 1530 mEndTime.normalize(true); 1531 mTimezoneAdapter.setCurrentTimezone(mTimezone); 1532 } 1533 1534 // TextWatcher interface 1535 @Override 1536 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 1537 } 1538 1539 // TextWatcher interface 1540 @Override 1541 public void onTextChanged(CharSequence s, int start, int before, int count) { 1542 } 1543 1544 // TextWatcher interface 1545 @Override 1546 public void afterTextChanged(Editable s) { 1547 mAddAttendeesButton.setEnabled(s.length() > 0); 1548 } 1549 1550 /** 1551 * @param isChecked 1552 */ 1553 protected void setAllDayViewsVisibility(boolean isChecked) { 1554 if (isChecked) { 1555 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 1556 mEndTime.monthDay--; 1557 long endMillis = mEndTime.normalize(true); 1558 1559 // Do not allow an event to have an end time 1560 // before the 1561 // start time. 1562 if (mEndTime.before(mStartTime)) { 1563 mEndTime.set(mStartTime); 1564 endMillis = mEndTime.normalize(true); 1565 } 1566 setDate(mEndDateButton, endMillis); 1567 setTime(mEndTimeButton, endMillis); 1568 } 1569 1570 mStartTimeButton.setVisibility(View.GONE); 1571 mEndTimeButton.setVisibility(View.GONE); 1572 mTimezoneButton.setVisibility(View.GONE); 1573 mTimezoneLabel.setVisibility(View.GONE); 1574 } else { 1575 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 1576 mEndTime.monthDay++; 1577 long endMillis = mEndTime.normalize(true); 1578 setDate(mEndDateButton, endMillis); 1579 setTime(mEndTimeButton, endMillis); 1580 } 1581 mStartTimeButton.setVisibility(View.VISIBLE); 1582 mEndTimeButton.setVisibility(View.VISIBLE); 1583 mTimezoneButton.setVisibility(View.VISIBLE); 1584 mTimezoneLabel.setVisibility(View.VISIBLE); 1585 } 1586 updateHomeTime(); 1587 } 1588 1589 @Override 1590 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1591 // This is only used for the Calendar spinner in new events, and only fires when the 1592 // calendar selection changes. 1593 Cursor c = (Cursor) parent.getItemAtPosition(position); 1594 if (c == null) { 1595 // TODO: can this happen? should we drop this check? 1596 Log.w(TAG, "Cursor not set on calendar item"); 1597 return; 1598 } 1599 1600 int colorColumn = c.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR); 1601 mColorChip.setBackgroundColor(c.getInt(colorColumn)); 1602 mModel.mCalendarColor = c.getInt(colorColumn); 1603 mCalendarsSpinner.setBackgroundColor(mModel.mCalendarColor); 1604 mCalendarSelectorGroup.setBackgroundColor(mModel.mCalendarColor); 1605 1606 1607 // Update the max/allowed reminders with the new calendar properties. 1608 int maxRemindersColumn = c.getColumnIndexOrThrow(Calendars.MAX_REMINDERS); 1609 mModel.mCalendarMaxReminders = c.getInt(maxRemindersColumn); 1610 int allowedRemindersColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_REMINDERS); 1611 mModel.mCalendarAllowedReminders = c.getString(allowedRemindersColumn); 1612 1613 // Discard the current reminders and replace them with the model's default reminder set. 1614 // We could attempt to save & restore the reminders that have been added, but that's 1615 // probably more trouble than it's worth. 1616 mModel.mReminders.clear(); 1617 mModel.mReminders.addAll(mModel.mDefaultReminders); 1618 mModel.mHasAlarm = mModel.mReminders.size() != 0; 1619 1620 // Update the UI elements. 1621 mReminderItems.clear(); 1622 LinearLayout reminderLayout = 1623 (LinearLayout) mScrollView.findViewById(R.id.reminder_items_container); 1624 reminderLayout.removeAllViews(); 1625 prepareReminders(); 1626 } 1627 1628 /** 1629 * Checks if the start and end times for this event should be displayed in 1630 * the Calendar app's time zone as well and formats and displays them. 1631 */ 1632 private void updateHomeTime() { 1633 String tz = Utils.getTimeZone(mActivity, null); 1634 if (!mAllDayCheckBox.isChecked() && !TextUtils.equals(tz, mTimezone) 1635 && mModification != EditEventHelper.MODIFY_UNINITIALIZED) { 1636 int flags = DateUtils.FORMAT_SHOW_TIME; 1637 boolean is24Format = DateFormat.is24HourFormat(mActivity); 1638 if (is24Format) { 1639 flags |= DateUtils.FORMAT_24HOUR; 1640 } 1641 long millisStart = mStartTime.toMillis(false); 1642 long millisEnd = mEndTime.toMillis(false); 1643 1644 boolean isDSTStart = mStartTime.isDst != 0; 1645 boolean isDSTEnd = mEndTime.isDst != 0; 1646 1647 // First update the start date and times 1648 String tzDisplay = TimeZone.getTimeZone(tz).getDisplayName( 1649 isDSTStart, TimeZone.SHORT, Locale.getDefault()); 1650 StringBuilder time = new StringBuilder(); 1651 1652 mSB.setLength(0); 1653 time.append(DateUtils 1654 .formatDateRange(mActivity, mF, millisStart, millisStart, flags, tz)) 1655 .append(" ").append(tzDisplay); 1656 mStartTimeHome.setText(time.toString()); 1657 1658 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE 1659 | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY; 1660 mSB.setLength(0); 1661 mStartDateHome 1662 .setText(DateUtils.formatDateRange( 1663 mActivity, mF, millisStart, millisStart, flags, tz).toString()); 1664 1665 // Make any adjustments needed for the end times 1666 if (isDSTEnd != isDSTStart) { 1667 tzDisplay = TimeZone.getTimeZone(tz).getDisplayName( 1668 isDSTEnd, TimeZone.SHORT, Locale.getDefault()); 1669 } 1670 flags = DateUtils.FORMAT_SHOW_TIME; 1671 if (is24Format) { 1672 flags |= DateUtils.FORMAT_24HOUR; 1673 } 1674 1675 // Then update the end times 1676 time.setLength(0); 1677 mSB.setLength(0); 1678 time.append(DateUtils.formatDateRange( 1679 mActivity, mF, millisEnd, millisEnd, flags, tz)).append(" ").append(tzDisplay); 1680 mEndTimeHome.setText(time.toString()); 1681 1682 flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE 1683 | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY; 1684 mSB.setLength(0); 1685 mEndDateHome.setText(DateUtils.formatDateRange( 1686 mActivity, mF, millisEnd, millisEnd, flags, tz).toString()); 1687 1688 mStartHomeGroup.setVisibility(View.VISIBLE); 1689 mEndHomeGroup.setVisibility(View.VISIBLE); 1690 } else { 1691 mStartHomeGroup.setVisibility(View.GONE); 1692 mEndHomeGroup.setVisibility(View.GONE); 1693 } 1694 } 1695 1696 @Override 1697 public void onNothingSelected(AdapterView<?> parent) { 1698 mColorChip.setBackgroundColor(0); 1699 1700 } 1701} 1702