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