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