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