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