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