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