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