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