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