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