/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calendar.event; import android.app.Activity; import android.app.AlertDialog; import android.app.DialogFragment; import android.app.FragmentManager; import android.app.ProgressDialog; import android.app.Service; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.provider.CalendarContract; import android.provider.CalendarContract.Attendees; import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Reminders; import android.provider.Settings; import android.text.InputFilter; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.text.format.Time; import android.text.util.Rfc822Tokenizer; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.MultiAutoCompleteTextView; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.ResourceCursorAdapter; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import com.android.calendar.CalendarEventModel; import com.android.calendar.CalendarEventModel.Attendee; import com.android.calendar.CalendarEventModel.ReminderEntry; import com.android.calendar.EmailAddressAdapter; import com.android.calendar.EventInfoFragment; import com.android.calendar.EventRecurrenceFormatter; import com.android.calendar.GeneralPreferences; import com.android.calendar.R; import com.android.calendar.RecipientAdapter; import com.android.calendar.Utils; import com.android.calendar.event.EditEventHelper.EditDoneRunnable; import com.android.calendar.recurrencepicker.RecurrencePickerDialog; import com.android.calendarcommon2.EventRecurrence; import com.android.common.Rfc822InputFilter; import com.android.common.Rfc822Validator; import com.android.datetimepicker.date.DatePickerDialog; import com.android.datetimepicker.date.DatePickerDialog.OnDateSetListener; import com.android.datetimepicker.time.RadialPickerLayout; import com.android.datetimepicker.time.TimePickerDialog; import com.android.datetimepicker.time.TimePickerDialog.OnTimeSetListener; import com.android.ex.chips.AccountSpecifier; import com.android.ex.chips.BaseRecipientAdapter; import com.android.ex.chips.ChipsUtil; import com.android.ex.chips.RecipientEditTextView; import com.android.timezonepicker.TimeZoneInfo; import com.android.timezonepicker.TimeZonePickerDialog; import com.android.timezonepicker.TimeZonePickerUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Formatter; import java.util.HashMap; import java.util.Locale; import java.util.TimeZone; public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener, DialogInterface.OnClickListener, OnItemSelectedListener, RecurrencePickerDialog.OnRecurrenceSetListener, TimeZonePickerDialog.OnTimeZoneSetListener { private static final String TAG = "EditEvent"; private static final String GOOGLE_SECONDARY_CALENDAR = "calendar.google.com"; private static final String PERIOD_SPACE = ". "; private static final String FRAG_TAG_DATE_PICKER = "datePickerDialogFragment"; private static final String FRAG_TAG_TIME_PICKER = "timePickerDialogFragment"; private static final String FRAG_TAG_TIME_ZONE_PICKER = "timeZonePickerDialogFragment"; private static final String FRAG_TAG_RECUR_PICKER = "recurrencePickerDialogFragment"; ArrayList mEditOnlyList = new ArrayList(); ArrayList mEditViewList = new ArrayList(); ArrayList mViewOnlyList = new ArrayList(); TextView mLoadingMessage; ScrollView mScrollView; Button mStartDateButton; Button mEndDateButton; Button mStartTimeButton; Button mEndTimeButton; Button mTimezoneButton; View mColorPickerNewEvent; View mColorPickerExistingEvent; OnClickListener mChangeColorOnClickListener; View mTimezoneRow; TextView mStartTimeHome; TextView mStartDateHome; TextView mEndTimeHome; TextView mEndDateHome; CheckBox mAllDayCheckBox; Spinner mCalendarsSpinner; Button mRruleButton; Spinner mAvailabilitySpinner; Spinner mAccessLevelSpinner; RadioGroup mResponseRadioGroup; TextView mTitleTextView; AutoCompleteTextView mLocationTextView; EventLocationAdapter mLocationAdapter; TextView mDescriptionTextView; TextView mWhenView; TextView mTimezoneTextView; TextView mTimezoneLabel; LinearLayout mRemindersContainer; MultiAutoCompleteTextView mAttendeesList; View mCalendarSelectorGroup; View mCalendarSelectorWrapper; View mCalendarStaticGroup; View mLocationGroup; View mDescriptionGroup; View mRemindersGroup; View mResponseGroup; View mOrganizerGroup; View mAttendeesGroup; View mStartHomeGroup; View mEndHomeGroup; private int[] mOriginalPadding = new int[4]; public boolean mIsMultipane; private ProgressDialog mLoadingCalendarsDialog; private AlertDialog mNoCalendarsDialog; private DialogFragment mTimezoneDialog; private Activity mActivity; private EditDoneRunnable mDone; private View mView; private CalendarEventModel mModel; private Cursor mCalendarsCursor; private AccountSpecifier mAddressAdapter; private Rfc822Validator mEmailValidator; public boolean mTimeSelectedWasStartTime; public boolean mDateSelectedWasStartDate; private TimePickerDialog mStartTimePickerDialog; private TimePickerDialog mEndTimePickerDialog; private DatePickerDialog mDatePickerDialog; /** * Contents of the "minutes" spinner. This has default values from the XML file, augmented * with any additional values that were already associated with the event. */ private ArrayList mReminderMinuteValues; private ArrayList mReminderMinuteLabels; /** * Contents of the "methods" spinner. The "values" list specifies the method constant * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels. Any methods that * aren't allowed by the Calendar will be removed. */ private ArrayList mReminderMethodValues; private ArrayList mReminderMethodLabels; /** * Contents of the "availability" spinner. The "values" list specifies the * type constant (e.g. {@link Events#AVAILABILITY_BUSY}) associated with the * labels. Any types that aren't allowed by the Calendar will be removed. */ private ArrayList mAvailabilityValues; private ArrayList mAvailabilityLabels; private ArrayList mOriginalAvailabilityLabels; private ArrayAdapter mAvailabilityAdapter; private boolean mAvailabilityExplicitlySet; private boolean mAllDayChangingAvailability; private int mAvailabilityCurrentlySelected; private int mDefaultReminderMinutes; private boolean mSaveAfterQueryComplete = false; private TimeZonePickerUtils mTzPickerUtils; private Time mStartTime; private Time mEndTime; private String mTimezone; private boolean mAllDay = false; private int mModification = EditEventHelper.MODIFY_UNINITIALIZED; private EventRecurrence mEventRecurrence = new EventRecurrence(); private ArrayList mReminderItems = new ArrayList(0); private ArrayList mUnsupportedReminders = new ArrayList(); private String mRrule; private static StringBuilder mSB = new StringBuilder(50); private static Formatter mF = new Formatter(mSB, Locale.getDefault()); /* This class is used to update the time buttons. */ private class TimeListener implements OnTimeSetListener { private View mView; public TimeListener(View view) { mView = view; } @Override public void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute) { // Cache the member variables locally to avoid inner class overhead. Time startTime = mStartTime; Time endTime = mEndTime; // Cache the start and end millis so that we limit the number // of calls to normalize() and toMillis(), which are fairly // expensive. long startMillis; long endMillis; if (mView == mStartTimeButton) { // The start time was changed. int hourDuration = endTime.hour - startTime.hour; int minuteDuration = endTime.minute - startTime.minute; startTime.hour = hourOfDay; startTime.minute = minute; startMillis = startTime.normalize(true); // Also update the end time to keep the duration constant. endTime.hour = hourOfDay + hourDuration; endTime.minute = minute + minuteDuration; // Update tz in case the start time switched from/to DLS populateTimezone(startMillis); } else { // The end time was changed. startMillis = startTime.toMillis(true); endTime.hour = hourOfDay; endTime.minute = minute; // Move to the start time if the end time is before the start // time. if (endTime.before(startTime)) { endTime.monthDay = startTime.monthDay + 1; } // Call populateTimezone if we support end time zone as well } endMillis = endTime.normalize(true); setDate(mEndDateButton, endMillis); setTime(mStartTimeButton, startMillis); setTime(mEndTimeButton, endMillis); updateHomeTime(); } } private class TimeClickListener implements View.OnClickListener { private Time mTime; public TimeClickListener(Time time) { mTime = time; } @Override public void onClick(View v) { TimePickerDialog dialog; if (v == mStartTimeButton) { mTimeSelectedWasStartTime = true; if (mStartTimePickerDialog == null) { mStartTimePickerDialog = TimePickerDialog.newInstance(new TimeListener(v), mTime.hour, mTime.minute, DateFormat.is24HourFormat(mActivity)); } else { mStartTimePickerDialog.setStartTime(mTime.hour, mTime.minute); } dialog = mStartTimePickerDialog; } else { mTimeSelectedWasStartTime = false; if (mEndTimePickerDialog == null) { mEndTimePickerDialog = TimePickerDialog.newInstance(new TimeListener(v), mTime.hour, mTime.minute, DateFormat.is24HourFormat(mActivity)); } else { mEndTimePickerDialog.setStartTime(mTime.hour, mTime.minute); } dialog = mEndTimePickerDialog; } final FragmentManager fm = mActivity.getFragmentManager(); fm.executePendingTransactions(); if (dialog != null && !dialog.isAdded()) { dialog.show(fm, FRAG_TAG_TIME_PICKER); } } } private class DateListener implements OnDateSetListener { View mView; public DateListener(View view) { mView = view; } @Override public void onDateSet(DatePickerDialog view, int year, int month, int monthDay) { Log.d(TAG, "onDateSet: " + year + " " + month + " " + monthDay); // Cache the member variables locally to avoid inner class overhead. Time startTime = mStartTime; Time endTime = mEndTime; // Cache the start and end millis so that we limit the number // of calls to normalize() and toMillis(), which are fairly // expensive. long startMillis; long endMillis; if (mView == mStartDateButton) { // The start date was changed. int yearDuration = endTime.year - startTime.year; int monthDuration = endTime.month - startTime.month; int monthDayDuration = endTime.monthDay - startTime.monthDay; startTime.year = year; startTime.month = month; startTime.monthDay = monthDay; startMillis = startTime.normalize(true); // Also update the end date to keep the duration constant. endTime.year = year + yearDuration; endTime.month = month + monthDuration; endTime.monthDay = monthDay + monthDayDuration; endMillis = endTime.normalize(true); // If the start date has changed then update the repeats. populateRepeats(); // Update tz in case the start time switched from/to DLS populateTimezone(startMillis); } else { // The end date was changed. startMillis = startTime.toMillis(true); endTime.year = year; endTime.month = month; endTime.monthDay = monthDay; endMillis = endTime.normalize(true); // Do not allow an event to have an end time before the start // time. if (endTime.before(startTime)) { endTime.set(startTime); endMillis = startMillis; } // Call populateTimezone if we support end time zone as well } setDate(mStartDateButton, startMillis); setDate(mEndDateButton, endMillis); setTime(mEndTimeButton, endMillis); // In case end time had to be // reset updateHomeTime(); } } // Fills in the date and time fields private void populateWhen() { long startMillis = mStartTime.toMillis(false /* use isDst */); long endMillis = mEndTime.toMillis(false /* use isDst */); setDate(mStartDateButton, startMillis); setDate(mEndDateButton, endMillis); setTime(mStartTimeButton, startMillis); setTime(mEndTimeButton, endMillis); mStartDateButton.setOnClickListener(new DateClickListener(mStartTime)); mEndDateButton.setOnClickListener(new DateClickListener(mEndTime)); mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime)); mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime)); } // Implements OnTimeZoneSetListener @Override public void onTimeZoneSet(TimeZoneInfo tzi) { setTimezone(tzi.mTzId); updateHomeTime(); } private void setTimezone(String timeZone) { mTimezone = timeZone; mStartTime.timezone = mTimezone; long timeMillis = mStartTime.normalize(true); mEndTime.timezone = mTimezone; mEndTime.normalize(true); populateTimezone(timeMillis); } private void populateTimezone(long eventStartTime) { if (mTzPickerUtils == null) { mTzPickerUtils = new TimeZonePickerUtils(mActivity); } CharSequence displayName = mTzPickerUtils.getGmtDisplayName(mActivity, mTimezone, eventStartTime, true); mTimezoneTextView.setText(displayName); mTimezoneButton.setText(displayName); } private void showTimezoneDialog() { Bundle b = new Bundle(); b.putLong(TimeZonePickerDialog.BUNDLE_START_TIME_MILLIS, mStartTime.toMillis(false)); b.putString(TimeZonePickerDialog.BUNDLE_TIME_ZONE, mTimezone); FragmentManager fm = mActivity.getFragmentManager(); TimeZonePickerDialog tzpd = (TimeZonePickerDialog) fm .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER); if (tzpd != null) { tzpd.dismiss(); } tzpd = new TimeZonePickerDialog(); tzpd.setArguments(b); tzpd.setOnTimeZoneSetListener(EditEventView.this); tzpd.show(fm, FRAG_TAG_TIME_ZONE_PICKER); } private void populateRepeats() { Resources r = mActivity.getResources(); String repeatString; boolean enabled; if (!TextUtils.isEmpty(mRrule)) { repeatString = EventRecurrenceFormatter.getRepeatString(mActivity, r, mEventRecurrence, true); if (repeatString == null) { repeatString = r.getString(R.string.custom); Log.e(TAG, "Can't generate display string for " + mRrule); enabled = false; } else { // TODO Should give option to clear/reset rrule enabled = RecurrencePickerDialog.canHandleRecurrenceRule(mEventRecurrence); if (!enabled) { Log.e(TAG, "UI can't handle " + mRrule); } } } else { repeatString = r.getString(R.string.does_not_repeat); enabled = true; } mRruleButton.setText(repeatString); // Don't allow the user to make exceptions recurring events. if (mModel.mOriginalSyncId != null) { enabled = false; } mRruleButton.setOnClickListener(this); mRruleButton.setEnabled(enabled); } private class DateClickListener implements View.OnClickListener { private Time mTime; public DateClickListener(Time time) { mTime = time; } @Override public void onClick(View v) { if (!mView.hasWindowFocus()) { // Don't do anything if the activity if paused. Since Activity doesn't // have a built in way to do this, we would have to implement one ourselves and // either cast our Activity to a specialized activity base class or implement some // generic interface that tells us if an activity is paused. hasWindowFocus() is // close enough if not quite perfect. return; } if (v == mStartDateButton) { mDateSelectedWasStartDate = true; } else { mDateSelectedWasStartDate = false; } final DateListener listener = new DateListener(v); if (mDatePickerDialog != null) { mDatePickerDialog.dismiss(); } mDatePickerDialog = DatePickerDialog.newInstance(listener, mTime.year, mTime.month, mTime.monthDay); mDatePickerDialog.setFirstDayOfWeek(Utils.getFirstDayOfWeekAsCalendar(mActivity)); mDatePickerDialog.setYearRange(Utils.YEAR_MIN, Utils.YEAR_MAX); mDatePickerDialog.show(mActivity.getFragmentManager(), FRAG_TAG_DATE_PICKER); } } public static class CalendarsAdapter extends ResourceCursorAdapter { public CalendarsAdapter(Context context, int resourceId, Cursor c) { super(context, resourceId, c); setDropDownViewResource(R.layout.calendars_dropdown_item); } @Override public void bindView(View view, Context context, Cursor cursor) { View colorBar = view.findViewById(R.id.color); int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR); int nameColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_DISPLAY_NAME); int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT); if (colorBar != null) { colorBar.setBackgroundColor(Utils.getDisplayColorFromColor(cursor .getInt(colorColumn))); } TextView name = (TextView) view.findViewById(R.id.calendar_name); if (name != null) { String displayName = cursor.getString(nameColumn); name.setText(displayName); TextView accountName = (TextView) view.findViewById(R.id.account_name); if (accountName != null) { accountName.setText(cursor.getString(ownerColumn)); accountName.setVisibility(TextView.VISIBLE); } } } } /** * Does prep steps for saving a calendar event. * * This triggers a parse of the attendees list and checks if the event is * ready to be saved. An event is ready to be saved so long as a model * exists and has a calendar it can be associated with, either because it's * an existing event or we've finished querying. * * @return false if there is no model or no calendar had been loaded yet, * true otherwise. */ public boolean prepareForSave() { if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) { return false; } return fillModelFromUI(); } public boolean fillModelFromReadOnlyUi() { if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) { return false; } mModel.mReminders = EventViewUtils.reminderItemsToReminders( mReminderItems, mReminderMinuteValues, mReminderMethodValues); mModel.mReminders.addAll(mUnsupportedReminders); mModel.normalizeReminders(); int status = EventInfoFragment.getResponseFromButtonId( mResponseRadioGroup.getCheckedRadioButtonId()); if (status != Attendees.ATTENDEE_STATUS_NONE) { mModel.mSelfAttendeeStatus = status; } return true; } // This is called if the user clicks on one of the buttons: "Save", // "Discard", or "Delete". This is also called if the user clicks // on the "remove reminder" button. @Override public void onClick(View view) { if (view == mRruleButton) { Bundle b = new Bundle(); b.putLong(RecurrencePickerDialog.BUNDLE_START_TIME_MILLIS, mStartTime.toMillis(false)); b.putString(RecurrencePickerDialog.BUNDLE_TIME_ZONE, mStartTime.timezone); // TODO may be more efficient to serialize and pass in EventRecurrence b.putString(RecurrencePickerDialog.BUNDLE_RRULE, mRrule); FragmentManager fm = mActivity.getFragmentManager(); RecurrencePickerDialog rpd = (RecurrencePickerDialog) fm .findFragmentByTag(FRAG_TAG_RECUR_PICKER); if (rpd != null) { rpd.dismiss(); } rpd = new RecurrencePickerDialog(); rpd.setArguments(b); rpd.setOnRecurrenceSetListener(EditEventView.this); rpd.show(fm, FRAG_TAG_RECUR_PICKER); return; } // This must be a click on one of the "remove reminder" buttons LinearLayout reminderItem = (LinearLayout) view.getParent(); LinearLayout parent = (LinearLayout) reminderItem.getParent(); parent.removeView(reminderItem); mReminderItems.remove(reminderItem); updateRemindersVisibility(mReminderItems.size()); EventViewUtils.updateAddReminderButton(mView, mReminderItems, mModel.mCalendarMaxReminders); } @Override public void onRecurrenceSet(String rrule) { Log.d(TAG, "Old rrule:" + mRrule); Log.d(TAG, "New rrule:" + rrule); mRrule = rrule; if (mRrule != null) { mEventRecurrence.parse(mRrule); } populateRepeats(); } // This is called if the user cancels the "No calendars" dialog. // The "No calendars" dialog is shown if there are no syncable calendars. @Override public void onCancel(DialogInterface dialog) { if (dialog == mLoadingCalendarsDialog) { mLoadingCalendarsDialog = null; mSaveAfterQueryComplete = false; } else if (dialog == mNoCalendarsDialog) { mDone.setDoneCode(Utils.DONE_REVERT); mDone.run(); return; } } // This is called if the user clicks on a dialog button. @Override public void onClick(DialogInterface dialog, int which) { if (dialog == mNoCalendarsDialog) { mDone.setDoneCode(Utils.DONE_REVERT); mDone.run(); if (which == DialogInterface.BUTTON_POSITIVE) { Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT); final String[] array = {"com.android.calendar"}; nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array); nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); mActivity.startActivity(nextIntent); } } } // Goes through the UI elements and updates the model as necessary private boolean fillModelFromUI() { if (mModel == null) { return false; } mModel.mReminders = EventViewUtils.reminderItemsToReminders(mReminderItems, mReminderMinuteValues, mReminderMethodValues); mModel.mReminders.addAll(mUnsupportedReminders); mModel.normalizeReminders(); mModel.mHasAlarm = mReminderItems.size() > 0; mModel.mTitle = mTitleTextView.getText().toString(); mModel.mAllDay = mAllDayCheckBox.isChecked(); mModel.mLocation = mLocationTextView.getText().toString(); mModel.mDescription = mDescriptionTextView.getText().toString(); if (TextUtils.isEmpty(mModel.mLocation)) { mModel.mLocation = null; } if (TextUtils.isEmpty(mModel.mDescription)) { mModel.mDescription = null; } int status = EventInfoFragment.getResponseFromButtonId(mResponseRadioGroup .getCheckedRadioButtonId()); if (status != Attendees.ATTENDEE_STATUS_NONE) { mModel.mSelfAttendeeStatus = status; } if (mAttendeesList != null) { mEmailValidator.setRemoveInvalid(true); mAttendeesList.performValidation(); mModel.mAttendeesList.clear(); mModel.addAttendees(mAttendeesList.getText().toString(), mEmailValidator); mEmailValidator.setRemoveInvalid(false); } // If this was a new event we need to fill in the Calendar information if (mModel.mUri == null) { mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId(); int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition(); if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) { String defaultCalendar = mCalendarsCursor.getString( EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT); Utils.setSharedPreference( mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, defaultCalendar); mModel.mOwnerAccount = defaultCalendar; mModel.mOrganizer = defaultCalendar; mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID); } } if (mModel.mAllDay) { // Reset start and end time, increment the monthDay by 1, and set // the timezone to UTC, as required for all-day events. mTimezone = Time.TIMEZONE_UTC; mStartTime.hour = 0; mStartTime.minute = 0; mStartTime.second = 0; mStartTime.timezone = mTimezone; mModel.mStart = mStartTime.normalize(true); mEndTime.hour = 0; mEndTime.minute = 0; mEndTime.second = 0; mEndTime.timezone = mTimezone; // When a user see the event duration as "X - Y" (e.g. Oct. 28 - Oct. 29), end time // should be Y + 1 (Oct.30). final long normalizedEndTimeMillis = mEndTime.normalize(true) + DateUtils.DAY_IN_MILLIS; if (normalizedEndTimeMillis < mModel.mStart) { // mEnd should be midnight of the next day of mStart. mModel.mEnd = mModel.mStart + DateUtils.DAY_IN_MILLIS; } else { mModel.mEnd = normalizedEndTimeMillis; } } else { mStartTime.timezone = mTimezone; mEndTime.timezone = mTimezone; mModel.mStart = mStartTime.toMillis(true); mModel.mEnd = mEndTime.toMillis(true); } mModel.mTimezone = mTimezone; mModel.mAccessLevel = mAccessLevelSpinner.getSelectedItemPosition(); // TODO set correct availability value mModel.mAvailability = mAvailabilityValues.get(mAvailabilitySpinner .getSelectedItemPosition()); // rrrule // If we're making an exception we don't want it to be a repeating // event. if (mModification == EditEventHelper.MODIFY_SELECTED) { mModel.mRrule = null; } else { mModel.mRrule = mRrule; } return true; } public EditEventView(Activity activity, View view, EditDoneRunnable done, boolean timeSelectedWasStartTime, boolean dateSelectedWasStartDate) { mActivity = activity; mView = view; mDone = done; // cache top level view elements mLoadingMessage = (TextView) view.findViewById(R.id.loading_message); mScrollView = (ScrollView) view.findViewById(R.id.scroll_view); // cache all the widgets mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars_spinner); mTitleTextView = (TextView) view.findViewById(R.id.title); mLocationTextView = (AutoCompleteTextView) view.findViewById(R.id.location); mDescriptionTextView = (TextView) view.findViewById(R.id.description); mTimezoneLabel = (TextView) view.findViewById(R.id.timezone_label); mStartDateButton = (Button) view.findViewById(R.id.start_date); mEndDateButton = (Button) view.findViewById(R.id.end_date); mWhenView = (TextView) mView.findViewById(R.id.when); mTimezoneTextView = (TextView) mView.findViewById(R.id.timezone_textView); mStartTimeButton = (Button) view.findViewById(R.id.start_time); mEndTimeButton = (Button) view.findViewById(R.id.end_time); mTimezoneButton = (Button) view.findViewById(R.id.timezone_button); mTimezoneButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showTimezoneDialog(); } }); mTimezoneRow = view.findViewById(R.id.timezone_button_row); mStartTimeHome = (TextView) view.findViewById(R.id.start_time_home_tz); mStartDateHome = (TextView) view.findViewById(R.id.start_date_home_tz); mEndTimeHome = (TextView) view.findViewById(R.id.end_time_home_tz); mEndDateHome = (TextView) view.findViewById(R.id.end_date_home_tz); mAllDayCheckBox = (CheckBox) view.findViewById(R.id.is_all_day); mRruleButton = (Button) view.findViewById(R.id.rrule); mAvailabilitySpinner = (Spinner) view.findViewById(R.id.availability); mAccessLevelSpinner = (Spinner) view.findViewById(R.id.visibility); mCalendarSelectorGroup = view.findViewById(R.id.calendar_selector_group); mCalendarSelectorWrapper = view.findViewById(R.id.calendar_selector_wrapper); mCalendarStaticGroup = view.findViewById(R.id.calendar_group); mRemindersGroup = view.findViewById(R.id.reminders_row); mResponseGroup = view.findViewById(R.id.response_row); mOrganizerGroup = view.findViewById(R.id.organizer_row); mAttendeesGroup = view.findViewById(R.id.add_attendees_row); mLocationGroup = view.findViewById(R.id.where_row); mDescriptionGroup = view.findViewById(R.id.description_row); mStartHomeGroup = view.findViewById(R.id.from_row_home_tz); mEndHomeGroup = view.findViewById(R.id.to_row_home_tz); mAttendeesList = (MultiAutoCompleteTextView) view.findViewById(R.id.attendees); mColorPickerNewEvent = view.findViewById(R.id.change_color_new_event); mColorPickerExistingEvent = view.findViewById(R.id.change_color_existing_event); mTitleTextView.setTag(mTitleTextView.getBackground()); mLocationTextView.setTag(mLocationTextView.getBackground()); mLocationAdapter = new EventLocationAdapter(activity); mLocationTextView.setAdapter(mLocationAdapter); mLocationTextView.setOnEditorActionListener(new OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { // Dismiss the suggestions dropdown. Return false so the other // side effects still occur (soft keyboard going away, etc.). mLocationTextView.dismissDropDown(); } return false; } }); mAvailabilityExplicitlySet = false; mAllDayChangingAvailability = false; mAvailabilityCurrentlySelected = -1; mAvailabilitySpinner.setOnItemSelectedListener( new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { // The spinner's onItemSelected gets called while it is being // initialized to the first item, and when we explicitly set it // in the allDay checkbox toggling, so we need these checks to // find out when the spinner is actually being clicked. // Set the initial selection. if (mAvailabilityCurrentlySelected == -1) { mAvailabilityCurrentlySelected = position; } if (mAvailabilityCurrentlySelected != position && !mAllDayChangingAvailability) { mAvailabilityExplicitlySet = true; } else { mAvailabilityCurrentlySelected = position; mAllDayChangingAvailability = false; } } @Override public void onNothingSelected(AdapterView arg0) { } }); mDescriptionTextView.setTag(mDescriptionTextView.getBackground()); mAttendeesList.setTag(mAttendeesList.getBackground()); mOriginalPadding[0] = mLocationTextView.getPaddingLeft(); mOriginalPadding[1] = mLocationTextView.getPaddingTop(); mOriginalPadding[2] = mLocationTextView.getPaddingRight(); mOriginalPadding[3] = mLocationTextView.getPaddingBottom(); mEditViewList.add(mTitleTextView); mEditViewList.add(mLocationTextView); mEditViewList.add(mDescriptionTextView); mEditViewList.add(mAttendeesList); mViewOnlyList.add(view.findViewById(R.id.when_row)); mViewOnlyList.add(view.findViewById(R.id.timezone_textview_row)); mEditOnlyList.add(view.findViewById(R.id.all_day_row)); mEditOnlyList.add(view.findViewById(R.id.availability_row)); mEditOnlyList.add(view.findViewById(R.id.visibility_row)); mEditOnlyList.add(view.findViewById(R.id.from_row)); mEditOnlyList.add(view.findViewById(R.id.to_row)); mEditOnlyList.add(mTimezoneRow); mEditOnlyList.add(mStartHomeGroup); mEditOnlyList.add(mEndHomeGroup); mResponseRadioGroup = (RadioGroup) view.findViewById(R.id.response_value); mRemindersContainer = (LinearLayout) view.findViewById(R.id.reminder_items_container); mTimezone = Utils.getTimeZone(activity, null); mIsMultipane = activity.getResources().getBoolean(R.bool.tablet_config); mStartTime = new Time(mTimezone); mEndTime = new Time(mTimezone); mEmailValidator = new Rfc822Validator(null); initMultiAutoCompleteTextView((RecipientEditTextView) mAttendeesList); // Display loading screen setModel(null); FragmentManager fm = activity.getFragmentManager(); RecurrencePickerDialog rpd = (RecurrencePickerDialog) fm .findFragmentByTag(FRAG_TAG_RECUR_PICKER); if (rpd != null) { rpd.setOnRecurrenceSetListener(this); } TimeZonePickerDialog tzpd = (TimeZonePickerDialog) fm .findFragmentByTag(FRAG_TAG_TIME_ZONE_PICKER); if (tzpd != null) { tzpd.setOnTimeZoneSetListener(this); } TimePickerDialog tpd = (TimePickerDialog) fm.findFragmentByTag(FRAG_TAG_TIME_PICKER); if (tpd != null) { View v; mTimeSelectedWasStartTime = timeSelectedWasStartTime; if (timeSelectedWasStartTime) { v = mStartTimeButton; } else { v = mEndTimeButton; } tpd.setOnTimeSetListener(new TimeListener(v)); } mDatePickerDialog = (DatePickerDialog) fm.findFragmentByTag(FRAG_TAG_DATE_PICKER); if (mDatePickerDialog != null) { View v; mDateSelectedWasStartDate = dateSelectedWasStartDate; if (dateSelectedWasStartDate) { v = mStartDateButton; } else { v = mEndDateButton; } mDatePickerDialog.setOnDateSetListener(new DateListener(v)); } } /** * Loads an integer array asset into a list. */ private static ArrayList loadIntegerArray(Resources r, int resNum) { int[] vals = r.getIntArray(resNum); int size = vals.length; ArrayList list = new ArrayList(size); for (int i = 0; i < size; i++) { list.add(vals[i]); } return list; } /** * Loads a String array asset into a list. */ private static ArrayList loadStringArray(Resources r, int resNum) { String[] labels = r.getStringArray(resNum); ArrayList list = new ArrayList(Arrays.asList(labels)); return list; } private void prepareAvailability() { Resources r = mActivity.getResources(); mAvailabilityValues = loadIntegerArray(r, R.array.availability_values); mAvailabilityLabels = loadStringArray(r, R.array.availability); // Copy the unadulterated availability labels for all-day toggling. mOriginalAvailabilityLabels = new ArrayList(); mOriginalAvailabilityLabels.addAll(mAvailabilityLabels); if (mModel.mCalendarAllowedAvailability != null) { EventViewUtils.reduceMethodList(mAvailabilityValues, mAvailabilityLabels, mModel.mCalendarAllowedAvailability); } mAvailabilityAdapter = new ArrayAdapter(mActivity, android.R.layout.simple_spinner_item, mAvailabilityLabels); mAvailabilityAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); mAvailabilitySpinner.setAdapter(mAvailabilityAdapter); } /** * Prepares the reminder UI elements. *

* (Re-)loads the minutes / methods lists from the XML assets, adds/removes items as * needed for the current set of reminders and calendar properties, and then creates UI * elements. */ private void prepareReminders() { CalendarEventModel model = mModel; Resources r = mActivity.getResources(); // Load the labels and corresponding numeric values for the minutes and methods lists // from the assets. If we're switching calendars, we need to clear and re-populate the // lists (which may have elements added and removed based on calendar properties). This // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a // new event that aren't in the default set. mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values); mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels); mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values); mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels); // Remove any reminder methods that aren't allowed for this calendar. If this is // a new event, mCalendarAllowedReminders may not be set the first time we're called. if (mModel.mCalendarAllowedReminders != null) { EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels, mModel.mCalendarAllowedReminders); } int numReminders = 0; if (model.mHasAlarm) { ArrayList reminders = model.mReminders; numReminders = reminders.size(); // Insert any minute values that aren't represented in the minutes list. for (ReminderEntry re : reminders) { if (mReminderMethodValues.contains(re.getMethod())) { EventViewUtils.addMinutesToList(mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes()); } } // Create a UI element for each reminder. We display all of the reminders we get // from the provider, even if the count exceeds the calendar maximum. (Also, for // a new event, we won't have a maxReminders value available.) mUnsupportedReminders.clear(); for (ReminderEntry re : reminders) { if (mReminderMethodValues.contains(re.getMethod()) || re.getMethod() == Reminders.METHOD_DEFAULT) { EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues, mReminderMethodLabels, re, Integer.MAX_VALUE, null); } else { // TODO figure out a way to display unsupported reminders mUnsupportedReminders.add(re); } } } updateRemindersVisibility(numReminders); EventViewUtils.updateAddReminderButton(mView, mReminderItems, mModel.mCalendarMaxReminders); } /** * Fill in the view with the contents of the given event model. This allows * an edit view to be initialized before the event has been loaded. Passing * in null for the model will display a loading screen. A non-null model * will fill in the view's fields with the data contained in the model. * * @param model The event model to pull the data from */ public void setModel(CalendarEventModel model) { mModel = model; // Need to close the autocomplete adapter to prevent leaking cursors. if (mAddressAdapter != null && mAddressAdapter instanceof EmailAddressAdapter) { ((EmailAddressAdapter)mAddressAdapter).close(); mAddressAdapter = null; } if (model == null) { // Display loading screen mLoadingMessage.setVisibility(View.VISIBLE); mScrollView.setVisibility(View.GONE); return; } boolean canRespond = EditEventHelper.canRespond(model); long begin = model.mStart; long end = model.mEnd; mTimezone = model.mTimezone; // this will be UTC for all day events // Set up the starting times if (begin > 0) { mStartTime.timezone = mTimezone; mStartTime.set(begin); mStartTime.normalize(true); } if (end > 0) { mEndTime.timezone = mTimezone; mEndTime.set(end); mEndTime.normalize(true); } mRrule = model.mRrule; if (!TextUtils.isEmpty(mRrule)) { mEventRecurrence.parse(mRrule); } if (mEventRecurrence.startDate == null) { mEventRecurrence.startDate = mStartTime; } // If the user is allowed to change the attendees set up the view and // validator if (!model.mHasAttendeeData) { mAttendeesGroup.setVisibility(View.GONE); } mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { setAllDayViewsVisibility(isChecked); } }); boolean prevAllDay = mAllDayCheckBox.isChecked(); mAllDay = false; // default to false. Let setAllDayViewsVisibility update it as needed if (model.mAllDay) { mAllDayCheckBox.setChecked(true); // put things back in local time for all day events mTimezone = Utils.getTimeZone(mActivity, null); mStartTime.timezone = mTimezone; mEndTime.timezone = mTimezone; mEndTime.normalize(true); } else { mAllDayCheckBox.setChecked(false); } // On a rotation we need to update the views but onCheckedChanged // doesn't get called if (prevAllDay == mAllDayCheckBox.isChecked()) { setAllDayViewsVisibility(prevAllDay); } populateTimezone(mStartTime.normalize(true)); SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity); String defaultReminderString = prefs.getString( GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING); mDefaultReminderMinutes = Integer.parseInt(defaultReminderString); prepareReminders(); prepareAvailability(); View reminderAddButton = mView.findViewById(R.id.reminder_add); View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { addReminder(); } }; reminderAddButton.setOnClickListener(addReminderOnClickListener); if (!mIsMultipane) { mView.findViewById(R.id.is_all_day_label).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { mAllDayCheckBox.setChecked(!mAllDayCheckBox.isChecked()); } }); } if (model.mTitle != null) { mTitleTextView.setTextKeepState(model.mTitle); } if (model.mIsOrganizer || TextUtils.isEmpty(model.mOrganizer) || model.mOrganizer.endsWith(GOOGLE_SECONDARY_CALENDAR)) { mView.findViewById(R.id.organizer_label).setVisibility(View.GONE); mView.findViewById(R.id.organizer).setVisibility(View.GONE); mOrganizerGroup.setVisibility(View.GONE); } else { ((TextView) mView.findViewById(R.id.organizer)).setText(model.mOrganizerDisplayName); } if (model.mLocation != null) { mLocationTextView.setTextKeepState(model.mLocation); } if (model.mDescription != null) { mDescriptionTextView.setTextKeepState(model.mDescription); } int availIndex = mAvailabilityValues.indexOf(model.mAvailability); if (availIndex != -1) { mAvailabilitySpinner.setSelection(availIndex); } mAccessLevelSpinner.setSelection(model.mAccessLevel); View responseLabel = mView.findViewById(R.id.response_label); if (canRespond) { int buttonToCheck = EventInfoFragment .findButtonIdForResponse(model.mSelfAttendeeStatus); mResponseRadioGroup.check(buttonToCheck); // -1 clear all radio buttons mResponseRadioGroup.setVisibility(View.VISIBLE); responseLabel.setVisibility(View.VISIBLE); } else { responseLabel.setVisibility(View.GONE); mResponseRadioGroup.setVisibility(View.GONE); mResponseGroup.setVisibility(View.GONE); } if (model.mUri != null) { // This is an existing event so hide the calendar spinner // since we can't change the calendar. View calendarGroup = mView.findViewById(R.id.calendar_selector_group); calendarGroup.setVisibility(View.GONE); TextView tv = (TextView) mView.findViewById(R.id.calendar_textview); tv.setText(model.mCalendarDisplayName); tv = (TextView) mView.findViewById(R.id.calendar_textview_secondary); if (tv != null) { tv.setText(model.mOwnerAccount); } } else { View calendarGroup = mView.findViewById(R.id.calendar_group); calendarGroup.setVisibility(View.GONE); } if (model.isEventColorInitialized()) { updateHeadlineColor(model, model.getEventColor()); } populateWhen(); populateRepeats(); updateAttendees(model.mAttendeesList); updateView(); mScrollView.setVisibility(View.VISIBLE); mLoadingMessage.setVisibility(View.GONE); sendAccessibilityEvent(); } public void updateHeadlineColor(CalendarEventModel model, int displayColor) { if (model.mUri != null) { if (mIsMultipane) { mView.findViewById(R.id.calendar_textview_with_colorpicker) .setBackgroundColor(displayColor); } else { mView.findViewById(R.id.calendar_group).setBackgroundColor(displayColor); } } else { setSpinnerBackgroundColor(displayColor); } } private void setSpinnerBackgroundColor(int displayColor) { if (mIsMultipane) { mCalendarSelectorWrapper.setBackgroundColor(displayColor); } else { mCalendarSelectorGroup.setBackgroundColor(displayColor); } } private void sendAccessibilityEvent() { AccessibilityManager am = (AccessibilityManager) mActivity.getSystemService(Service.ACCESSIBILITY_SERVICE); if (!am.isEnabled() || mModel == null) { return; } StringBuilder b = new StringBuilder(); addFieldsRecursive(b, mView); CharSequence msg = b.toString(); AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); event.setClassName(getClass().getName()); event.setPackageName(mActivity.getPackageName()); event.getText().add(msg); event.setAddedCount(msg.length()); am.sendAccessibilityEvent(event); } private void addFieldsRecursive(StringBuilder b, View v) { if (v == null || v.getVisibility() != View.VISIBLE) { return; } if (v instanceof TextView) { CharSequence tv = ((TextView) v).getText(); if (!TextUtils.isEmpty(tv.toString().trim())) { b.append(tv + PERIOD_SPACE); } } else if (v instanceof RadioGroup) { RadioGroup rg = (RadioGroup) v; int id = rg.getCheckedRadioButtonId(); if (id != View.NO_ID) { b.append(((RadioButton) (v.findViewById(id))).getText() + PERIOD_SPACE); } } else if (v instanceof Spinner) { Spinner s = (Spinner) v; if (s.getSelectedItem() instanceof String) { String str = ((String) (s.getSelectedItem())).trim(); if (!TextUtils.isEmpty(str)) { b.append(str + PERIOD_SPACE); } } } else if (v instanceof ViewGroup) { ViewGroup vg = (ViewGroup) v; int children = vg.getChildCount(); for (int i = 0; i < children; i++) { addFieldsRecursive(b, vg.getChildAt(i)); } } } /** * Creates a single line string for the time/duration */ protected void setWhenString() { String when; int flags = DateUtils.FORMAT_SHOW_DATE; String tz = mTimezone; if (mModel.mAllDay) { flags |= DateUtils.FORMAT_SHOW_WEEKDAY; tz = Time.TIMEZONE_UTC; } else { flags |= DateUtils.FORMAT_SHOW_TIME; if (DateFormat.is24HourFormat(mActivity)) { flags |= DateUtils.FORMAT_24HOUR; } } long startMillis = mStartTime.normalize(true); long endMillis = mEndTime.normalize(true); mSB.setLength(0); when = DateUtils .formatDateRange(mActivity, mF, startMillis, endMillis, flags, tz).toString(); mWhenView.setText(when); } /** * Configures the Calendars spinner. This is only done for new events, because only new * events allow you to select a calendar while editing an event. *

* We tuck a reference to a Cursor with calendar database data into the spinner, so that * we can easily extract calendar-specific values when the value changes (the spinner's * onItemSelected callback is configured). */ public void setCalendarsCursor(Cursor cursor, boolean userVisible, long selectedCalendarId) { // If there are no syncable calendars, then we cannot allow // creating a new event. mCalendarsCursor = cursor; if (cursor == null || cursor.getCount() == 0) { // Cancel the "loading calendars" dialog if it exists if (mSaveAfterQueryComplete) { mLoadingCalendarsDialog.cancel(); } if (!userVisible) { return; } // Create an error message for the user that, when clicked, // will exit this activity without saving the event. AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); builder.setTitle(R.string.no_syncable_calendars).setIconAttribute( android.R.attr.alertDialogIcon).setMessage(R.string.no_calendars_found) .setPositiveButton(R.string.add_account, this) .setNegativeButton(android.R.string.no, this).setOnCancelListener(this); mNoCalendarsDialog = builder.show(); return; } int selection; if (selectedCalendarId != -1) { selection = findSelectedCalendarPosition(cursor, selectedCalendarId); } else { selection = findDefaultCalendarPosition(cursor); } // populate the calendars spinner CalendarsAdapter adapter = new CalendarsAdapter(mActivity, R.layout.calendars_spinner_item, cursor); mCalendarsSpinner.setAdapter(adapter); mCalendarsSpinner.setOnItemSelectedListener(this); mCalendarsSpinner.setSelection(selection); if (mSaveAfterQueryComplete) { mLoadingCalendarsDialog.cancel(); if (prepareForSave() && fillModelFromUI()) { int exit = userVisible ? Utils.DONE_EXIT : 0; mDone.setDoneCode(Utils.DONE_SAVE | exit); mDone.run(); } else if (userVisible) { mDone.setDoneCode(Utils.DONE_EXIT); mDone.run(); } else if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "SetCalendarsCursor:Save failed and unable to exit view"); } return; } } /** * Updates the view based on {@link #mModification} and {@link #mModel} */ public void updateView() { if (mModel == null) { return; } if (EditEventHelper.canModifyEvent(mModel)) { setViewStates(mModification); } else { setViewStates(Utils.MODIFY_UNINITIALIZED); } } private void setViewStates(int mode) { // Extra canModify check just in case if (mode == Utils.MODIFY_UNINITIALIZED || !EditEventHelper.canModifyEvent(mModel)) { setWhenString(); for (View v : mViewOnlyList) { v.setVisibility(View.VISIBLE); } for (View v : mEditOnlyList) { v.setVisibility(View.GONE); } for (View v : mEditViewList) { v.setEnabled(false); v.setBackgroundDrawable(null); } mCalendarSelectorGroup.setVisibility(View.GONE); mCalendarStaticGroup.setVisibility(View.VISIBLE); mRruleButton.setEnabled(false); if (EditEventHelper.canAddReminders(mModel)) { mRemindersGroup.setVisibility(View.VISIBLE); } else { mRemindersGroup.setVisibility(View.GONE); } if (TextUtils.isEmpty(mLocationTextView.getText())) { mLocationGroup.setVisibility(View.GONE); } if (TextUtils.isEmpty(mDescriptionTextView.getText())) { mDescriptionGroup.setVisibility(View.GONE); } } else { for (View v : mViewOnlyList) { v.setVisibility(View.GONE); } for (View v : mEditOnlyList) { v.setVisibility(View.VISIBLE); } for (View v : mEditViewList) { v.setEnabled(true); if (v.getTag() != null) { v.setBackgroundDrawable((Drawable) v.getTag()); v.setPadding(mOriginalPadding[0], mOriginalPadding[1], mOriginalPadding[2], mOriginalPadding[3]); } } if (mModel.mUri == null) { mCalendarSelectorGroup.setVisibility(View.VISIBLE); mCalendarStaticGroup.setVisibility(View.GONE); } else { mCalendarSelectorGroup.setVisibility(View.GONE); mCalendarStaticGroup.setVisibility(View.VISIBLE); } if (mModel.mOriginalSyncId == null) { mRruleButton.setEnabled(true); } else { mRruleButton.setEnabled(false); mRruleButton.setBackgroundDrawable(null); } mRemindersGroup.setVisibility(View.VISIBLE); mLocationGroup.setVisibility(View.VISIBLE); mDescriptionGroup.setVisibility(View.VISIBLE); } setAllDayViewsVisibility(mAllDayCheckBox.isChecked()); } public void setModification(int modifyWhich) { mModification = modifyWhich; updateView(); updateHomeTime(); } private int findSelectedCalendarPosition(Cursor calendarsCursor, long calendarId) { if (calendarsCursor.getCount() <= 0) { return -1; } int calendarIdColumn = calendarsCursor.getColumnIndexOrThrow(Calendars._ID); int position = 0; calendarsCursor.moveToPosition(-1); while (calendarsCursor.moveToNext()) { if (calendarsCursor.getLong(calendarIdColumn) == calendarId) { return position; } position++; } return 0; } // Find the calendar position in the cursor that matches calendar in // preference private int findDefaultCalendarPosition(Cursor calendarsCursor) { if (calendarsCursor.getCount() <= 0) { return -1; } String defaultCalendar = Utils.getSharedPreference( mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, (String) null); int calendarsOwnerIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT); int accountNameIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME); int accountTypeIndex = calendarsCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE); int position = 0; calendarsCursor.moveToPosition(-1); while (calendarsCursor.moveToNext()) { String calendarOwner = calendarsCursor.getString(calendarsOwnerIndex); if (defaultCalendar == null) { // There is no stored default upon the first time running. Use a primary // calendar in this case. if (calendarOwner != null && calendarOwner.equals(calendarsCursor.getString(accountNameIndex)) && !CalendarContract.ACCOUNT_TYPE_LOCAL.equals( calendarsCursor.getString(accountTypeIndex))) { return position; } } else if (defaultCalendar.equals(calendarOwner)) { // Found the default calendar. return position; } position++; } return 0; } private void updateAttendees(HashMap attendeesList) { if (attendeesList == null || attendeesList.isEmpty()) { return; } mAttendeesList.setText(null); for (Attendee attendee : attendeesList.values()) { // TODO: Please remove separator when Calendar uses the chips MR2 project // Adding a comma separator between email addresses to prevent a chips MR1.1 bug // in which email addresses are concatenated together with no separator. mAttendeesList.append(attendee.mEmail + ", "); } } private void updateRemindersVisibility(int numReminders) { if (numReminders == 0) { mRemindersContainer.setVisibility(View.GONE); } else { mRemindersContainer.setVisibility(View.VISIBLE); } } /** * Add a new reminder when the user hits the "add reminder" button. We use the default * reminder time and method. */ private void addReminder() { // TODO: when adding a new reminder, make it different from the // last one in the list (if any). if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) { EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues, mReminderMethodLabels, ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), mModel.mCalendarMaxReminders, null); } else { EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems, mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues, mReminderMethodLabels, ReminderEntry.valueOf(mDefaultReminderMinutes), mModel.mCalendarMaxReminders, null); } updateRemindersVisibility(mReminderItems.size()); EventViewUtils.updateAddReminderButton(mView, mReminderItems, mModel.mCalendarMaxReminders); } // From com.google.android.gm.ComposeActivity private MultiAutoCompleteTextView initMultiAutoCompleteTextView(RecipientEditTextView list) { if (ChipsUtil.supportsChipsUi()) { mAddressAdapter = new RecipientAdapter(mActivity); list.setAdapter((BaseRecipientAdapter) mAddressAdapter); list.setOnFocusListShrinkRecipients(false); } else { mAddressAdapter = new EmailAddressAdapter(mActivity); list.setAdapter((EmailAddressAdapter)mAddressAdapter); } list.setTokenizer(new Rfc822Tokenizer()); list.setValidator(mEmailValidator); // NOTE: assumes no other filters are set list.setFilters(sRecipientFilters); return list; } /** * From com.google.android.gm.ComposeActivity Implements special address * cleanup rules: The first space key entry following an "@" symbol that is * followed by any combination of letters and symbols, including one+ dots * and zero commas, should insert an extra comma (followed by the space). */ private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() }; private void setDate(TextView view, long millis) { int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_ABBREV_WEEKDAY; // Unfortunately, DateUtils doesn't support a timezone other than the // default timezone provided by the system, so we have this ugly hack // here to trick it into formatting our time correctly. In order to // prevent all sorts of craziness, we synchronize on the TimeZone class // to prevent other threads from reading an incorrect timezone from // calls to TimeZone#getDefault() // TODO fix this if/when DateUtils allows for passing in a timezone String dateString; synchronized (TimeZone.class) { TimeZone.setDefault(TimeZone.getTimeZone(mTimezone)); dateString = DateUtils.formatDateTime(mActivity, millis, flags); // setting the default back to null restores the correct behavior TimeZone.setDefault(null); } view.setText(dateString); } private void setTime(TextView view, long millis) { int flags = DateUtils.FORMAT_SHOW_TIME; flags |= DateUtils.FORMAT_CAP_NOON_MIDNIGHT; if (DateFormat.is24HourFormat(mActivity)) { flags |= DateUtils.FORMAT_24HOUR; } // Unfortunately, DateUtils doesn't support a timezone other than the // default timezone provided by the system, so we have this ugly hack // here to trick it into formatting our time correctly. In order to // prevent all sorts of craziness, we synchronize on the TimeZone class // to prevent other threads from reading an incorrect timezone from // calls to TimeZone#getDefault() // TODO fix this if/when DateUtils allows for passing in a timezone String timeString; synchronized (TimeZone.class) { TimeZone.setDefault(TimeZone.getTimeZone(mTimezone)); timeString = DateUtils.formatDateTime(mActivity, millis, flags); TimeZone.setDefault(null); } view.setText(timeString); } /** * @param isChecked */ protected void setAllDayViewsVisibility(boolean isChecked) { if (isChecked) { if (mEndTime.hour == 0 && mEndTime.minute == 0) { if (mAllDay != isChecked) { mEndTime.monthDay--; } long endMillis = mEndTime.normalize(true); // Do not allow an event to have an end time // before the // start time. if (mEndTime.before(mStartTime)) { mEndTime.set(mStartTime); endMillis = mEndTime.normalize(true); } setDate(mEndDateButton, endMillis); setTime(mEndTimeButton, endMillis); } mStartTimeButton.setVisibility(View.GONE); mEndTimeButton.setVisibility(View.GONE); mTimezoneRow.setVisibility(View.GONE); } else { if (mEndTime.hour == 0 && mEndTime.minute == 0) { if (mAllDay != isChecked) { mEndTime.monthDay++; } long endMillis = mEndTime.normalize(true); setDate(mEndDateButton, endMillis); setTime(mEndTimeButton, endMillis); } mStartTimeButton.setVisibility(View.VISIBLE); mEndTimeButton.setVisibility(View.VISIBLE); mTimezoneRow.setVisibility(View.VISIBLE); } // If this is a new event, and if availability has not yet been // explicitly set, toggle busy/available as the inverse of all day. if (mModel.mUri == null && !mAvailabilityExplicitlySet) { // Values are from R.arrays.availability_values. // 0 = busy // 1 = available int newAvailabilityValue = isChecked? 1 : 0; if (mAvailabilityAdapter != null && mAvailabilityValues != null && mAvailabilityValues.contains(newAvailabilityValue)) { // We'll need to let the spinner's listener know that we're // explicitly toggling it. mAllDayChangingAvailability = true; String newAvailabilityLabel = mOriginalAvailabilityLabels.get(newAvailabilityValue); int newAvailabilityPos = mAvailabilityAdapter.getPosition(newAvailabilityLabel); mAvailabilitySpinner.setSelection(newAvailabilityPos); } } mAllDay = isChecked; updateHomeTime(); } public void setColorPickerButtonStates(int[] colorArray) { setColorPickerButtonStates(colorArray != null && colorArray.length > 0); } public void setColorPickerButtonStates(boolean showColorPalette) { if (showColorPalette) { mColorPickerNewEvent.setVisibility(View.VISIBLE); mColorPickerExistingEvent.setVisibility(View.VISIBLE); } else { mColorPickerNewEvent.setVisibility(View.INVISIBLE); mColorPickerExistingEvent.setVisibility(View.GONE); } } public boolean isColorPaletteVisible() { return mColorPickerNewEvent.getVisibility() == View.VISIBLE || mColorPickerExistingEvent.getVisibility() == View.VISIBLE; } @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { // This is only used for the Calendar spinner in new events, and only fires when the // calendar selection changes or on screen rotation Cursor c = (Cursor) parent.getItemAtPosition(position); if (c == null) { // TODO: can this happen? should we drop this check? Log.w(TAG, "Cursor not set on calendar item"); return; } // Do nothing if the selection didn't change so that reminders will not get lost int idColumn = c.getColumnIndexOrThrow(Calendars._ID); long calendarId = c.getLong(idColumn); int colorColumn = c.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR); int color = c.getInt(colorColumn); int displayColor = Utils.getDisplayColorFromColor(color); // Prevents resetting of data (reminders, etc.) on orientation change. if (calendarId == mModel.mCalendarId && mModel.isCalendarColorInitialized() && displayColor == mModel.getCalendarColor()) { return; } setSpinnerBackgroundColor(displayColor); mModel.mCalendarId = calendarId; mModel.setCalendarColor(displayColor); mModel.mCalendarAccountName = c.getString(EditEventHelper.CALENDARS_INDEX_ACCOUNT_NAME); mModel.mCalendarAccountType = c.getString(EditEventHelper.CALENDARS_INDEX_ACCOUNT_TYPE); mModel.setEventColor(mModel.getCalendarColor()); setColorPickerButtonStates(mModel.getCalendarEventColors()); // Update the max/allowed reminders with the new calendar properties. int maxRemindersColumn = c.getColumnIndexOrThrow(Calendars.MAX_REMINDERS); mModel.mCalendarMaxReminders = c.getInt(maxRemindersColumn); int allowedRemindersColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_REMINDERS); mModel.mCalendarAllowedReminders = c.getString(allowedRemindersColumn); int allowedAttendeeTypesColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_ATTENDEE_TYPES); mModel.mCalendarAllowedAttendeeTypes = c.getString(allowedAttendeeTypesColumn); int allowedAvailabilityColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_AVAILABILITY); mModel.mCalendarAllowedAvailability = c.getString(allowedAvailabilityColumn); // Discard the current reminders and replace them with the model's default reminder set. // We could attempt to save & restore the reminders that have been added, but that's // probably more trouble than it's worth. mModel.mReminders.clear(); mModel.mReminders.addAll(mModel.mDefaultReminders); mModel.mHasAlarm = mModel.mReminders.size() != 0; // Update the UI elements. mReminderItems.clear(); LinearLayout reminderLayout = (LinearLayout) mScrollView.findViewById(R.id.reminder_items_container); reminderLayout.removeAllViews(); prepareReminders(); prepareAvailability(); } /** * Checks if the start and end times for this event should be displayed in * the Calendar app's time zone as well and formats and displays them. */ private void updateHomeTime() { String tz = Utils.getTimeZone(mActivity, null); if (!mAllDayCheckBox.isChecked() && !TextUtils.equals(tz, mTimezone) && mModification != EditEventHelper.MODIFY_UNINITIALIZED) { int flags = DateUtils.FORMAT_SHOW_TIME; boolean is24Format = DateFormat.is24HourFormat(mActivity); if (is24Format) { flags |= DateUtils.FORMAT_24HOUR; } long millisStart = mStartTime.toMillis(false); long millisEnd = mEndTime.toMillis(false); boolean isDSTStart = mStartTime.isDst != 0; boolean isDSTEnd = mEndTime.isDst != 0; // First update the start date and times String tzDisplay = TimeZone.getTimeZone(tz).getDisplayName( isDSTStart, TimeZone.SHORT, Locale.getDefault()); StringBuilder time = new StringBuilder(); mSB.setLength(0); time.append(DateUtils .formatDateRange(mActivity, mF, millisStart, millisStart, flags, tz)) .append(" ").append(tzDisplay); mStartTimeHome.setText(time.toString()); flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY; mSB.setLength(0); mStartDateHome .setText(DateUtils.formatDateRange( mActivity, mF, millisStart, millisStart, flags, tz).toString()); // Make any adjustments needed for the end times if (isDSTEnd != isDSTStart) { tzDisplay = TimeZone.getTimeZone(tz).getDisplayName( isDSTEnd, TimeZone.SHORT, Locale.getDefault()); } flags = DateUtils.FORMAT_SHOW_TIME; if (is24Format) { flags |= DateUtils.FORMAT_24HOUR; } // Then update the end times time.setLength(0); mSB.setLength(0); time.append(DateUtils.formatDateRange( mActivity, mF, millisEnd, millisEnd, flags, tz)).append(" ").append(tzDisplay); mEndTimeHome.setText(time.toString()); flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY; mSB.setLength(0); mEndDateHome.setText(DateUtils.formatDateRange( mActivity, mF, millisEnd, millisEnd, flags, tz).toString()); mStartHomeGroup.setVisibility(View.VISIBLE); mEndHomeGroup.setVisibility(View.VISIBLE); } else { mStartHomeGroup.setVisibility(View.GONE); mEndHomeGroup.setVisibility(View.GONE); } } @Override public void onNothingSelected(AdapterView parent) { } }