EditEventView.java revision ce8a833592bc0c44e8be4d5aec5813d364cd0b30
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            new TimePickerDialog(mActivity, new TimeListener(v), mTime.hour, mTime.minute,
259                    DateFormat.is24HourFormat(mActivity)).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.show();
386    }
387
388    private void populateRepeats() {
389        Time time = mStartTime;
390        Resources r = mActivity.getResources();
391        int resource = android.R.layout.simple_spinner_item;
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, resource, repeatArray);
487        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
488        mRepeatsSpinner.setAdapter(adapter);
489        mRepeatsSpinner.setSelection(position);
490
491        // Don't allow the user to make exceptions recurring events.
492        if (mModel.mOriginalSyncId != null) {
493            mRepeatsSpinner.setEnabled(false);
494        }
495    }
496
497    private boolean isCustomRecurrence() {
498
499        if (mEventRecurrence.until != null
500                || (mEventRecurrence.interval != 0 && mEventRecurrence.interval != 1)
501                || mEventRecurrence.count != 0) {
502            return true;
503        }
504
505        if (mEventRecurrence.freq == 0) {
506            return false;
507        }
508
509        switch (mEventRecurrence.freq) {
510            case EventRecurrence.DAILY:
511                return false;
512            case EventRecurrence.WEEKLY:
513                if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
514                    return false;
515                } else if (mEventRecurrence.bydayCount == 1) {
516                    return false;
517                }
518                break;
519            case EventRecurrence.MONTHLY:
520                if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
521                    /* this is a "3rd Tuesday of every month" sort of rule */
522                    return false;
523                } else if (mEventRecurrence.bydayCount == 0
524                        && mEventRecurrence.bymonthdayCount == 1
525                        && mEventRecurrence.bymonthday[0] > 0) {
526                    /* this is a "22nd day of every month" sort of rule */
527                    return false;
528                }
529                break;
530            case EventRecurrence.YEARLY:
531                return false;
532        }
533
534        return true;
535    }
536
537    private boolean isWeekdayEvent() {
538        if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {
539            return true;
540        }
541        return false;
542    }
543
544    private class DateClickListener implements View.OnClickListener {
545        private Time mTime;
546
547        public DateClickListener(Time time) {
548            mTime = time;
549        }
550
551        public void onClick(View v) {
552            DatePickerDialog dpd = new DatePickerDialog(
553                    mActivity, new DateListener(v), mTime.year, mTime.month, mTime.monthDay);
554            CalendarView cv = dpd.getDatePicker().getCalendarView();
555            cv.setShowWeekNumber(Utils.getShowWeekNumber(mActivity));
556            int startOfWeek = Utils.getFirstDayOfWeek(mActivity);
557            // Utils returns Time days while CalendarView wants Calendar days
558            if (startOfWeek == Time.SATURDAY) {
559                startOfWeek = Calendar.SATURDAY;
560            } else if (startOfWeek == Time.SUNDAY) {
561                startOfWeek = Calendar.SUNDAY;
562            } else {
563                startOfWeek = Calendar.MONDAY;
564            }
565            cv.setFirstDayOfWeek(startOfWeek);
566            dpd.show();
567        }
568    }
569
570    static private class CalendarsAdapter extends ResourceCursorAdapter {
571        public CalendarsAdapter(Context context, Cursor c) {
572            super(context, R.layout.calendars_item, c);
573            setDropDownViewResource(R.layout.calendars_dropdown_item);
574        }
575
576        @Override
577        public void bindView(View view, Context context, Cursor cursor) {
578            View colorBar = view.findViewById(R.id.color);
579            int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
580            int nameColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_DISPLAY_NAME);
581            int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
582            if (colorBar != null) {
583                colorBar.setBackgroundColor(Utils.getDisplayColorFromColor(cursor
584                        .getInt(colorColumn)));
585            }
586
587            TextView name = (TextView) view.findViewById(R.id.calendar_name);
588            if (name != null) {
589                String displayName = cursor.getString(nameColumn);
590                name.setText(displayName);
591
592                TextView accountName = (TextView) view.findViewById(R.id.account_name);
593                if (accountName != null) {
594                    Resources res = context.getResources();
595                    accountName.setText(cursor.getString(ownerColumn));
596                    accountName.setVisibility(TextView.VISIBLE);
597                }
598            }
599        }
600    }
601
602    /**
603     * Does prep steps for saving a calendar event.
604     *
605     * This triggers a parse of the attendees list and checks if the event is
606     * ready to be saved. An event is ready to be saved so long as a model
607     * exists and has a calendar it can be associated with, either because it's
608     * an existing event or we've finished querying.
609     *
610     * @return false if there is no model or no calendar had been loaded yet,
611     * true otherwise.
612     */
613    public boolean prepareForSave() {
614        if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) {
615            return false;
616        }
617        return fillModelFromUI();
618    }
619
620    public boolean fillModelFromReadOnlyUi() {
621        if (mModel == null || (mCalendarsCursor == null && mModel.mUri == null)) {
622            return false;
623        }
624        mModel.mReminders = EventViewUtils.reminderItemsToReminders(
625                    mReminderItems, mReminderMinuteValues, mReminderMethodValues);
626        int status = EventInfoFragment.getResponseFromButtonId(
627                mResponseRadioGroup.getCheckedRadioButtonId());
628        if (status != Attendees.ATTENDEE_STATUS_NONE) {
629            mModel.mSelfAttendeeStatus = status;
630        }
631        return true;
632    }
633
634    // This is called if the user clicks on one of the buttons: "Save",
635    // "Discard", or "Delete". This is also called if the user clicks
636    // on the "remove reminder" button.
637    @Override
638    public void onClick(View view) {
639
640        // This must be a click on one of the "remove reminder" buttons
641        LinearLayout reminderItem = (LinearLayout) view.getParent();
642        LinearLayout parent = (LinearLayout) reminderItem.getParent();
643        parent.removeView(reminderItem);
644        mReminderItems.remove(reminderItem);
645        updateRemindersVisibility(mReminderItems.size());
646    }
647
648    // This is called if the user cancels the "No calendars" dialog.
649    // The "No calendars" dialog is shown if there are no syncable calendars.
650    @Override
651    public void onCancel(DialogInterface dialog) {
652        if (dialog == mLoadingCalendarsDialog) {
653            mLoadingCalendarsDialog = null;
654            mSaveAfterQueryComplete = false;
655        } else if (dialog == mNoCalendarsDialog) {
656            mDone.setDoneCode(Utils.DONE_REVERT);
657            mDone.run();
658            return;
659        }
660    }
661
662    // This is called if the user clicks on a dialog button.
663    @Override
664    public void onClick(DialogInterface dialog, int which) {
665        if (dialog == mNoCalendarsDialog) {
666            mDone.setDoneCode(Utils.DONE_REVERT);
667            mDone.run();
668            if (which == DialogInterface.BUTTON_POSITIVE) {
669                Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT);
670                final String[] array = {"com.android.calendar"};
671                nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array);
672                nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
673                mActivity.startActivity(nextIntent);
674            }
675        } else if (dialog == mTimezoneDialog) {
676            if (which >= 0 && which < mTimezoneAdapter.getCount()) {
677                setTimezone(which);
678                updateHomeTime();
679                dialog.dismiss();
680            }
681        }
682    }
683
684    // Goes through the UI elements and updates the model as necessary
685    private boolean fillModelFromUI() {
686        if (mModel == null) {
687            return false;
688        }
689        mModel.mReminders = EventViewUtils.reminderItemsToReminders(mReminderItems,
690                mReminderMinuteValues, mReminderMethodValues);
691        mModel.mHasAlarm = mReminderItems.size() > 0;
692        mModel.mTitle = mTitleTextView.getText().toString();
693        mModel.mAllDay = mAllDayCheckBox.isChecked();
694        mModel.mLocation = mLocationTextView.getText().toString();
695        mModel.mDescription = mDescriptionTextView.getText().toString();
696        if (TextUtils.isEmpty(mModel.mLocation)) {
697            mModel.mLocation = null;
698        }
699        if (TextUtils.isEmpty(mModel.mDescription)) {
700            mModel.mDescription = null;
701        }
702
703        int status = EventInfoFragment.getResponseFromButtonId(mResponseRadioGroup
704                .getCheckedRadioButtonId());
705        if (status != Attendees.ATTENDEE_STATUS_NONE) {
706            mModel.mSelfAttendeeStatus = status;
707        }
708
709        if (mAttendeesList != null) {
710            mEmailValidator.setRemoveInvalid(true);
711            mAttendeesList.performValidation();
712            mModel.mAttendeesList.clear();
713            mModel.addAttendees(mAttendeesList.getText().toString(), mEmailValidator);
714            mEmailValidator.setRemoveInvalid(false);
715        }
716
717        // If this was a new event we need to fill in the Calendar information
718        if (mModel.mUri == null) {
719            mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId();
720            int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
721            if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
722                String defaultCalendar = mCalendarsCursor.getString(
723                        EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
724                Utils.setSharedPreference(
725                        mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, defaultCalendar);
726                mModel.mOwnerAccount = defaultCalendar;
727                mModel.mOrganizer = defaultCalendar;
728                mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID);
729            }
730        }
731
732        if (mModel.mAllDay) {
733            // Reset start and end time, increment the monthDay by 1, and set
734            // the timezone to UTC, as required for all-day events.
735            mTimezone = Time.TIMEZONE_UTC;
736            mStartTime.hour = 0;
737            mStartTime.minute = 0;
738            mStartTime.second = 0;
739            mStartTime.timezone = mTimezone;
740            mModel.mStart = mStartTime.normalize(true);
741
742            mEndTime.hour = 0;
743            mEndTime.minute = 0;
744            mEndTime.second = 0;
745            mEndTime.timezone = mTimezone;
746            // When a user see the event duration as "X - Y" (e.g. Oct. 28 - Oct. 29), end time
747            // should be Y + 1 (Oct.30).
748            final long normalizedEndTimeMillis =
749                    mEndTime.normalize(true) + DateUtils.DAY_IN_MILLIS;
750            if (normalizedEndTimeMillis < mModel.mStart) {
751                // mEnd should be midnight of the next day of mStart.
752                mModel.mEnd = mModel.mStart + DateUtils.DAY_IN_MILLIS;
753            } else {
754                mModel.mEnd = normalizedEndTimeMillis;
755            }
756        } else {
757            mStartTime.timezone = mTimezone;
758            mEndTime.timezone = mTimezone;
759            mModel.mStart = mStartTime.toMillis(true);
760            mModel.mEnd = mEndTime.toMillis(true);
761        }
762        mModel.mTimezone = mTimezone;
763        mModel.mAccessLevel = mAccessLevelSpinner.getSelectedItemPosition();
764        mModel.mAvailability = mAvailabilitySpinner.getSelectedItemPosition() != 0;
765
766        int selection;
767        // If we're making an exception we don't want it to be a repeating
768        // event.
769        if (mModification == EditEventHelper.MODIFY_SELECTED) {
770            selection = EditEventHelper.DOES_NOT_REPEAT;
771        } else {
772            int position = mRepeatsSpinner.getSelectedItemPosition();
773            selection = mRecurrenceIndexes.get(position);
774        }
775
776        EditEventHelper.updateRecurrenceRule(
777                selection, mModel, Utils.getFirstDayOfWeek(mActivity) + 1);
778
779        // Save the timezone so we can display it as a standard option next time
780        if (!mModel.mAllDay) {
781            mTimezoneAdapter.saveRecentTimezone(mTimezone);
782        }
783        return true;
784    }
785
786    public EditEventView(Activity activity, View view, EditDoneRunnable done) {
787
788        mActivity = activity;
789        mView = view;
790        mDone = done;
791
792        DEFAULT_DOMAIN = activity.getResources().getString(R.string.google_email_domain);
793
794        // cache top level view elements
795        mLoadingMessage = (TextView) view.findViewById(R.id.loading_message);
796        mScrollView = (ScrollView) view.findViewById(R.id.scroll_view);
797
798        // cache all the widgets
799        mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars_spinner);
800        mTitleTextView = (TextView) view.findViewById(R.id.title);
801        mLocationTextView = (TextView) view.findViewById(R.id.location);
802        mDescriptionTextView = (TextView) view.findViewById(R.id.description);
803        mTimezoneLabel = (TextView) view.findViewById(R.id.timezone_label);
804        mStartDateButton = (Button) view.findViewById(R.id.start_date);
805        mEndDateButton = (Button) view.findViewById(R.id.end_date);
806        mWhenView = (TextView) mView.findViewById(R.id.when);
807        mTimezoneTextView = (TextView) mView.findViewById(R.id.timezone_textView);
808        mStartTimeButton = (Button) view.findViewById(R.id.start_time);
809        mEndTimeButton = (Button) view.findViewById(R.id.end_time);
810        mTimezoneButton = (Button) view.findViewById(R.id.timezone_button);
811        mStartTimeHome = (TextView) view.findViewById(R.id.start_time_home_tz);
812        mStartDateHome = (TextView) view.findViewById(R.id.start_date_home_tz);
813        mEndTimeHome = (TextView) view.findViewById(R.id.end_time_home_tz);
814        mEndDateHome = (TextView) view.findViewById(R.id.end_date_home_tz);
815        mAllDayCheckBox = (CheckBox) view.findViewById(R.id.is_all_day);
816        mRepeatsSpinner = (Spinner) view.findViewById(R.id.repeats);
817        mAvailabilitySpinner = (Spinner) view.findViewById(R.id.availability);
818        mAccessLevelSpinner = (Spinner) view.findViewById(R.id.visibility);
819        mCalendarSelectorGroup = view.findViewById(R.id.calendar_selector_group);
820        mCalendarStaticGroup = view.findViewById(R.id.calendar_group);
821        mRemindersGroup = view.findViewById(R.id.reminders_row);
822        mResponseGroup = view.findViewById(R.id.response_row);
823        mOrganizerGroup = view.findViewById(R.id.organizer_row);
824        mAttendeesPane = view.findViewById(R.id.attendees_group);
825        mLocationGroup = view.findViewById(R.id.where_row);
826        mDescriptionGroup = view.findViewById(R.id.description_row);
827        mStartHomeGroup = view.findViewById(R.id.from_row_home_tz);
828        mEndHomeGroup = view.findViewById(R.id.to_row_home_tz);
829        mAttendeesList = (MultiAutoCompleteTextView) view.findViewById(R.id.attendees);
830
831        mTitleTextView.setTag(mTitleTextView.getBackground());
832        mLocationTextView.setTag(mLocationTextView.getBackground());
833        mDescriptionTextView.setTag(mDescriptionTextView.getBackground());
834        mRepeatsSpinner.setTag(mRepeatsSpinner.getBackground());
835        mAttendeesList.setTag(mAttendeesList.getBackground());
836        mOriginalPadding[0] = mLocationTextView.getPaddingLeft();
837        mOriginalPadding[1] = mLocationTextView.getPaddingTop();
838        mOriginalPadding[2] = mLocationTextView.getPaddingRight();
839        mOriginalPadding[3] = mLocationTextView.getPaddingBottom();
840        mEditViewList.add(mTitleTextView);
841        mEditViewList.add(mLocationTextView);
842        mEditViewList.add(mDescriptionTextView);
843        mEditViewList.add(mAttendeesList);
844
845        mViewOnlyList.add(view.findViewById(R.id.when_row));
846        mViewOnlyList.add(view.findViewById(R.id.timezone_textview_row));
847
848        mEditOnlyList.add(view.findViewById(R.id.all_day_row));
849        mEditOnlyList.add(view.findViewById(R.id.availability_row));
850        mEditOnlyList.add(view.findViewById(R.id.visibility_row));
851        mEditOnlyList.add(view.findViewById(R.id.from_row));
852        mEditOnlyList.add(view.findViewById(R.id.to_row));
853        mEditOnlyList.add(view.findViewById(R.id.timezone_button_row));
854        mEditOnlyList.add(mStartHomeGroup);
855        mEditOnlyList.add(mEndHomeGroup);
856
857        mResponseRadioGroup = (RadioGroup) view.findViewById(R.id.response_value);
858        mRemindersContainer = (LinearLayout) view.findViewById(R.id.reminder_items_container);
859
860        mTimezone = Utils.getTimeZone(activity, null);
861        mStartTime = new Time(mTimezone);
862        mEndTime = new Time(mTimezone);
863        mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
864        mEmailValidator = new Rfc822Validator(null);
865        initMultiAutoCompleteTextView((RecipientEditTextView) mAttendeesList);
866
867        mColorChip = view.findViewById(R.id.color_chip);
868
869        // Display loading screen
870        setModel(null);
871    }
872
873
874    /**
875     * Loads an integer array asset into a list.
876     */
877    private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) {
878        int[] vals = r.getIntArray(resNum);
879        int size = vals.length;
880        ArrayList<Integer> list = new ArrayList<Integer>(size);
881
882        for (int i = 0; i < size; i++) {
883            list.add(vals[i]);
884        }
885
886        return list;
887    }
888
889    /**
890     * Loads a String array asset into a list.
891     */
892    private static ArrayList<String> loadStringArray(Resources r, int resNum) {
893        String[] labels = r.getStringArray(resNum);
894        ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels));
895        return list;
896    }
897
898    /**
899     * Prepares the reminder UI elements.
900     * <p>
901     * (Re-)loads the minutes / methods lists from the XML assets, adds/removes items as
902     * needed for the current set of reminders and calendar properties, and then creates UI
903     * elements.
904     */
905    private void prepareReminders() {
906        CalendarEventModel model = mModel;
907        Resources r = mActivity.getResources();
908
909        // Load the labels and corresponding numeric values for the minutes and methods lists
910        // from the assets.  If we're switching calendars, we need to clear and re-populate the
911        // lists (which may have elements added and removed based on calendar properties).  This
912        // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a
913        // new event that aren't in the default set.
914        mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values);
915        mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels);
916        mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values);
917        mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels);
918
919        // Remove any reminder methods that aren't allowed for this calendar.  If this is
920        // a new event, mCalendarAllowedReminders may not be set the first time we're called.
921        if (mModel.mCalendarAllowedReminders != null) {
922            EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels,
923                    mModel.mCalendarAllowedReminders);
924        }
925
926        int numReminders = 0;
927        if (model.mHasAlarm) {
928            ArrayList<ReminderEntry> reminders = model.mReminders;
929            numReminders = reminders.size();
930            // Insert any minute values that aren't represented in the minutes list.
931            for (ReminderEntry re : reminders) {
932                EventViewUtils.addMinutesToList(
933                        mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes());
934            }
935
936            // Create a UI element for each reminder.  We display all of the reminders we get
937            // from the provider, even if the count exceeds the calendar maximum.  (Also, for
938            // a new event, we won't have a maxReminders value available.)
939            for (ReminderEntry re : reminders) {
940                EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
941                        mReminderMinuteValues, mReminderMinuteLabels,
942                        mReminderMethodValues, mReminderMethodLabels,
943                        re, Integer.MAX_VALUE);
944            }
945        }
946
947        updateRemindersVisibility(numReminders);
948    }
949
950    /**
951     * Fill in the view with the contents of the given event model. This allows
952     * an edit view to be initialized before the event has been loaded. Passing
953     * in null for the model will display a loading screen. A non-null model
954     * will fill in the view's fields with the data contained in the model.
955     *
956     * @param model The event model to pull the data from
957     */
958    public void setModel(CalendarEventModel model) {
959        mModel = model;
960
961        // Need to close the autocomplete adapter to prevent leaking cursors.
962        if (mAddressAdapter != null && mAddressAdapter instanceof EmailAddressAdapter) {
963            ((EmailAddressAdapter)mAddressAdapter).close();
964            mAddressAdapter = null;
965        }
966
967        if (model == null) {
968            // Display loading screen
969            mLoadingMessage.setVisibility(View.VISIBLE);
970            mScrollView.setVisibility(View.GONE);
971            return;
972        }
973
974        boolean canModifyCalendar = EditEventHelper.canModifyCalendar(model);
975        boolean canModifyEvent = EditEventHelper.canModifyEvent(model);
976        boolean canRespond = EditEventHelper.canRespond(model);
977
978        long begin = model.mStart;
979        long end = model.mEnd;
980        mTimezone = model.mTimezone; // this will be UTC for all day events
981
982        // Set up the starting times
983        if (begin > 0) {
984            mStartTime.timezone = mTimezone;
985            mStartTime.set(begin);
986            mStartTime.normalize(true);
987        }
988        if (end > 0) {
989            mEndTime.timezone = mTimezone;
990            mEndTime.set(end);
991            mEndTime.normalize(true);
992        }
993        String rrule = model.mRrule;
994        if (!TextUtils.isEmpty(rrule)) {
995            mEventRecurrence.parse(rrule);
996        }
997
998        // If the user is allowed to change the attendees set up the view and
999        // validator
1000        if (!model.mHasAttendeeData) {
1001            mAttendeesGroup.setVisibility(View.GONE);
1002        }
1003
1004        mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
1005            @Override
1006            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
1007                setAllDayViewsVisibility(isChecked);
1008            }
1009        });
1010
1011        if (model.mAllDay) {
1012            mAllDayCheckBox.setChecked(true);
1013            // put things back in local time for all day events
1014            mTimezone = TimeZone.getDefault().getID();
1015            mStartTime.timezone = mTimezone;
1016            mStartTime.normalize(true);
1017            mEndTime.timezone = mTimezone;
1018            mEndTime.normalize(true);
1019        } else {
1020            mAllDayCheckBox.setChecked(false);
1021        }
1022
1023        mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
1024        if (mTimezoneDialog != null) {
1025            mTimezoneDialog.getListView().setAdapter(mTimezoneAdapter);
1026        }
1027
1028        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
1029        String defaultReminderString = prefs.getString(
1030                GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING);
1031        mDefaultReminderMinutes = Integer.parseInt(defaultReminderString);
1032
1033        prepareReminders();
1034
1035        ImageButton reminderAddButton = (ImageButton) mView.findViewById(R.id.reminder_add);
1036        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
1037            @Override
1038            public void onClick(View v) {
1039                addReminder();
1040            }
1041        };
1042        reminderAddButton.setOnClickListener(addReminderOnClickListener);
1043
1044        mTitleTextView.setText(model.mTitle);
1045        if (model.mIsOrganizer || TextUtils.isEmpty(model.mOrganizer)
1046                || model.mOrganizer.endsWith(GOOGLE_SECONDARY_CALENDAR)) {
1047            mView.findViewById(R.id.organizer_label).setVisibility(View.GONE);
1048            mView.findViewById(R.id.organizer).setVisibility(View.GONE);
1049            mOrganizerGroup.setVisibility(View.GONE);
1050        } else {
1051            ((TextView) mView.findViewById(R.id.organizer)).setText(model.mOrganizerDisplayName);
1052        }
1053        mLocationTextView.setText(model.mLocation);
1054        mDescriptionTextView.setText(model.mDescription);
1055        mAvailabilitySpinner.setSelection(model.mAvailability ? 1 : 0);
1056        mAccessLevelSpinner.setSelection(model.mAccessLevel);
1057
1058        View responseLabel = mView.findViewById(R.id.response_label);
1059        if (canRespond) {
1060            int buttonToCheck = EventInfoFragment
1061                    .findButtonIdForResponse(model.mSelfAttendeeStatus);
1062            mResponseRadioGroup.check(buttonToCheck); // -1 clear all radio buttons
1063            mResponseRadioGroup.setVisibility(View.VISIBLE);
1064            responseLabel.setVisibility(View.VISIBLE);
1065        } else {
1066            responseLabel.setVisibility(View.GONE);
1067            mResponseRadioGroup.setVisibility(View.GONE);
1068            mResponseGroup.setVisibility(View.GONE);
1069        }
1070
1071        int displayColor = Utils.getDisplayColorFromColor(model.mCalendarColor);
1072        if (model.mUri != null) {
1073            // This is an existing event so hide the calendar spinner
1074            // since we can't change the calendar.
1075            View calendarGroup = mView.findViewById(R.id.calendar_selector_group);
1076            calendarGroup.setVisibility(View.GONE);
1077            TextView tv = (TextView) mView.findViewById(R.id.calendar_textview);
1078            tv.setText(model.mCalendarDisplayName);
1079            tv = (TextView) mView.findViewById(R.id.calendar_textview_secondary);
1080            if (tv != null) {
1081                tv.setText(model.mOwnerAccount);
1082            }
1083            mView.findViewById(R.id.calendar_group).setBackgroundColor(displayColor);
1084            mColorChip.setBackgroundColor(displayColor);
1085        } else {
1086            View calendarGroup = mView.findViewById(R.id.calendar_group);
1087            calendarGroup.setVisibility(View.GONE);
1088            mCalendarSelectorGroup.setBackgroundColor(displayColor);
1089        }
1090
1091        populateTimezone();
1092        populateWhen();
1093        populateRepeats();
1094        updateAttendees(model.mAttendeesList);
1095
1096        updateView();
1097        mScrollView.setVisibility(View.VISIBLE);
1098        mLoadingMessage.setVisibility(View.GONE);
1099        sendAccessibilityEvent();
1100    }
1101
1102    private void sendAccessibilityEvent() {
1103        AccessibilityManager am =
1104            (AccessibilityManager) mActivity.getSystemService(Service.ACCESSIBILITY_SERVICE);
1105        if (!am.isEnabled() || mModel == null) {
1106            return;
1107        }
1108        StringBuilder b = new StringBuilder();
1109        addFieldsRecursive(b, mView);
1110        CharSequence msg = b.toString();
1111
1112        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
1113        event.setClassName(getClass().getName());
1114        event.setPackageName(mActivity.getPackageName());
1115        event.getText().add(msg);
1116        event.setAddedCount(msg.length());
1117
1118        am.sendAccessibilityEvent(event);
1119    }
1120
1121    private void addFieldsRecursive(StringBuilder b, View v) {
1122        if (v == null || v.getVisibility() != View.VISIBLE) {
1123            return;
1124        }
1125        if (v instanceof TextView) {
1126            CharSequence tv = ((TextView) v).getText();
1127            if (!TextUtils.isEmpty(tv.toString().trim())) {
1128                b.append(tv + PERIOD_SPACE);
1129            }
1130        } else if (v instanceof RadioGroup) {
1131            RadioGroup rg = (RadioGroup) v;
1132            int id = rg.getCheckedRadioButtonId();
1133            if (id != View.NO_ID) {
1134                b.append(((RadioButton) (v.findViewById(id))).getText() + PERIOD_SPACE);
1135            }
1136        } else if (v instanceof Spinner) {
1137            Spinner s = (Spinner) v;
1138            if (s.getSelectedItem() instanceof String) {
1139                String str = ((String) (s.getSelectedItem())).trim();
1140                if (!TextUtils.isEmpty(str)) {
1141                    b.append(str + PERIOD_SPACE);
1142                }
1143            }
1144        } else if (v instanceof ViewGroup) {
1145            ViewGroup vg = (ViewGroup) v;
1146            int children = vg.getChildCount();
1147            for (int i = 0; i < children; i++) {
1148                addFieldsRecursive(b, vg.getChildAt(i));
1149            }
1150        }
1151    }
1152
1153    /**
1154     * Creates a single line string for the time/duration
1155     */
1156    protected void setWhenString() {
1157        String when;
1158        int flags = DateUtils.FORMAT_SHOW_DATE;
1159        String tz = mTimezone;
1160        if (mModel.mAllDay) {
1161            flags |= DateUtils.FORMAT_SHOW_WEEKDAY;
1162            tz = Time.TIMEZONE_UTC;
1163        } else {
1164            flags |= DateUtils.FORMAT_SHOW_TIME;
1165            if (DateFormat.is24HourFormat(mActivity)) {
1166                flags |= DateUtils.FORMAT_24HOUR;
1167            }
1168        }
1169        long startMillis = mStartTime.normalize(true);
1170        long endMillis = mEndTime.normalize(true);
1171        mSB.setLength(0);
1172        when = DateUtils
1173                .formatDateRange(mActivity, mF, startMillis, endMillis, flags, tz).toString();
1174        mWhenView.setText(when);
1175    }
1176
1177    /**
1178     * Configures the Calendars spinner.  This is only done for new events, because only new
1179     * events allow you to select a calendar while editing an event.
1180     * <p>
1181     * We tuck a reference to a Cursor with calendar database data into the spinner, so that
1182     * we can easily extract calendar-specific values when the value changes (the spinner's
1183     * onItemSelected callback is configured).
1184     */
1185    public void setCalendarsCursor(Cursor cursor, boolean userVisible) {
1186        // If there are no syncable calendars, then we cannot allow
1187        // creating a new event.
1188        mCalendarsCursor = cursor;
1189        if (cursor == null || cursor.getCount() == 0) {
1190            // Cancel the "loading calendars" dialog if it exists
1191            if (mSaveAfterQueryComplete) {
1192                mLoadingCalendarsDialog.cancel();
1193            }
1194            if (!userVisible) {
1195                return;
1196            }
1197            // Create an error message for the user that, when clicked,
1198            // will exit this activity without saving the event.
1199            AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
1200            builder.setTitle(R.string.no_syncable_calendars).setIconAttribute(
1201                    android.R.attr.alertDialogIcon).setMessage(R.string.no_calendars_found)
1202                    .setPositiveButton(R.string.add_account, this)
1203                    .setNegativeButton(android.R.string.no, this).setOnCancelListener(this);
1204            mNoCalendarsDialog = builder.show();
1205            return;
1206        }
1207
1208        int defaultCalendarPosition = findDefaultCalendarPosition(cursor);
1209
1210        // populate the calendars spinner
1211        CalendarsAdapter adapter = new CalendarsAdapter(mActivity, cursor);
1212        mCalendarsSpinner.setAdapter(adapter);
1213        mCalendarsSpinner.setSelection(defaultCalendarPosition);
1214        mCalendarsSpinner.setOnItemSelectedListener(this);
1215
1216        int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
1217        mColorChip.setBackgroundColor(Utils.getDisplayColorFromColor(cursor.getInt(colorColumn)));
1218
1219        if (mSaveAfterQueryComplete) {
1220            mLoadingCalendarsDialog.cancel();
1221            if (prepareForSave() && fillModelFromUI()) {
1222                int exit = userVisible ? Utils.DONE_EXIT : 0;
1223                mDone.setDoneCode(Utils.DONE_SAVE | exit);
1224                mDone.run();
1225            } else if (userVisible) {
1226                mDone.setDoneCode(Utils.DONE_EXIT);
1227                mDone.run();
1228            } else if (Log.isLoggable(TAG, Log.DEBUG)) {
1229                Log.d(TAG, "SetCalendarsCursor:Save failed and unable to exit view");
1230            }
1231            return;
1232        }
1233    }
1234
1235    /**
1236     * Updates the view based on {@link #mModification} and {@link #mModel}
1237     */
1238    public void updateView() {
1239        if (mModel == null) {
1240            return;
1241        }
1242        if (EditEventHelper.canModifyEvent(mModel)) {
1243            setViewStates(mModification);
1244        } else {
1245            setViewStates(Utils.MODIFY_UNINITIALIZED);
1246        }
1247    }
1248
1249    private void setViewStates(int mode) {
1250        // Extra canModify check just in case
1251        if (mode == Utils.MODIFY_UNINITIALIZED || !EditEventHelper.canModifyEvent(mModel)) {
1252            setWhenString();
1253
1254            for (View v : mViewOnlyList) {
1255                v.setVisibility(View.VISIBLE);
1256            }
1257            for (View v : mEditOnlyList) {
1258                v.setVisibility(View.GONE);
1259            }
1260            for (View v : mEditViewList) {
1261                v.setEnabled(false);
1262                v.setBackgroundDrawable(null);
1263            }
1264            mCalendarSelectorGroup.setVisibility(View.GONE);
1265            mCalendarStaticGroup.setVisibility(View.VISIBLE);
1266            mRepeatsSpinner.setEnabled(false);
1267            mRepeatsSpinner.setBackgroundDrawable(null);
1268            if (EditEventHelper.canAddReminders(mModel)) {
1269                mRemindersGroup.setVisibility(View.VISIBLE);
1270            } else {
1271                mRemindersGroup.setVisibility(View.GONE);
1272            }
1273            if (mAllDayCheckBox.isChecked()) {
1274                mView.findViewById(R.id.timezone_textview_row).setVisibility(View.GONE);
1275            }
1276            if (TextUtils.isEmpty(mLocationTextView.getText())) {
1277                mLocationGroup.setVisibility(View.GONE);
1278            }
1279            if (TextUtils.isEmpty(mDescriptionTextView.getText())) {
1280                mDescriptionGroup.setVisibility(View.GONE);
1281            }
1282        } else {
1283            for (View v : mViewOnlyList) {
1284                v.setVisibility(View.GONE);
1285            }
1286            for (View v : mEditOnlyList) {
1287                v.setVisibility(View.VISIBLE);
1288            }
1289            for (View v : mEditViewList) {
1290                v.setEnabled(true);
1291                if (v.getTag() != null) {
1292                    v.setBackgroundDrawable((Drawable) v.getTag());
1293                    v.setPadding(mOriginalPadding[0], mOriginalPadding[1], mOriginalPadding[2],
1294                            mOriginalPadding[3]);
1295                }
1296            }
1297            if (mModel.mUri == null) {
1298                mCalendarSelectorGroup.setVisibility(View.VISIBLE);
1299                mCalendarStaticGroup.setVisibility(View.GONE);
1300            } else {
1301                mCalendarSelectorGroup.setVisibility(View.GONE);
1302                mCalendarStaticGroup.setVisibility(View.VISIBLE);
1303            }
1304            mRepeatsSpinner.setBackgroundDrawable((Drawable) mRepeatsSpinner.getTag());
1305            mRepeatsSpinner.setPadding(mOriginalPadding[0], mOriginalPadding[1],
1306                    mOriginalPadding[2], mOriginalPadding[3]);
1307            if (mModel.mOriginalSyncId == null) {
1308                mRepeatsSpinner.setEnabled(true);
1309            } else {
1310                mRepeatsSpinner.setEnabled(false);
1311            }
1312            mRemindersGroup.setVisibility(View.VISIBLE);
1313            mAttendeesPane.setVisibility(View.VISIBLE);
1314
1315            mLocationGroup.setVisibility(View.VISIBLE);
1316            mDescriptionGroup.setVisibility(View.VISIBLE);
1317        }
1318    }
1319
1320    public void setModification(int modifyWhich) {
1321        mModification = modifyWhich;
1322        updateView();
1323        updateHomeTime();
1324    }
1325
1326    // Find the calendar position in the cursor that matches calendar in
1327    // preference
1328    private int findDefaultCalendarPosition(Cursor calendarsCursor) {
1329        if (calendarsCursor.getCount() <= 0) {
1330            return -1;
1331        }
1332
1333        String defaultCalendar = Utils.getSharedPreference(
1334                mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, null);
1335
1336        if (defaultCalendar == null) {
1337            return 0;
1338        }
1339        int calendarsOwnerColumn = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
1340        int position = 0;
1341        calendarsCursor.moveToPosition(-1);
1342        while (calendarsCursor.moveToNext()) {
1343            if (defaultCalendar.equals(calendarsCursor.getString(calendarsOwnerColumn))) {
1344                return position;
1345            }
1346            position++;
1347        }
1348        return 0;
1349    }
1350
1351    private void updateAttendees(HashMap<String, Attendee> attendeesList) {
1352        if (attendeesList == null || attendeesList.isEmpty()) {
1353            return;
1354        }
1355        mAttendeesList.setText(null);
1356        for (Attendee attendee : attendeesList.values()) {
1357            mAttendeesList.append(attendee.mEmail);
1358        }
1359    }
1360
1361    private void updateRemindersVisibility(int numReminders) {
1362        if (numReminders == 0) {
1363            mRemindersContainer.setVisibility(View.GONE);
1364        } else {
1365            mRemindersContainer.setVisibility(View.VISIBLE);
1366        }
1367    }
1368
1369    /**
1370     * Add a new reminder when the user hits the "add reminder" button.  We use the default
1371     * reminder time and method.
1372     */
1373    private void addReminder() {
1374        // TODO: when adding a new reminder, make it different from the
1375        // last one in the list (if any).
1376        if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) {
1377            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
1378                    mReminderMinuteValues, mReminderMinuteLabels,
1379                    mReminderMethodValues, mReminderMethodLabels,
1380                    ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME),
1381                    mModel.mCalendarMaxReminders);
1382        } else {
1383            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
1384                    mReminderMinuteValues, mReminderMinuteLabels,
1385                    mReminderMethodValues, mReminderMethodLabels,
1386                    ReminderEntry.valueOf(mDefaultReminderMinutes),
1387                    mModel.mCalendarMaxReminders);
1388        }
1389        updateRemindersVisibility(mReminderItems.size());
1390    }
1391
1392    // From com.google.android.gm.ComposeActivity
1393    private MultiAutoCompleteTextView initMultiAutoCompleteTextView(RecipientEditTextView list) {
1394        if (ChipsUtil.supportsChipsUi()) {
1395            mAddressAdapter = new RecipientAdapter(mActivity);
1396            list.setAdapter((BaseRecipientAdapter) mAddressAdapter);
1397            list.setOnFocusListShrinkRecipients(false);
1398            Resources r = mActivity.getResources();
1399            Bitmap def = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture);
1400            list.setChipDimensions(
1401                    r.getDrawable(R.drawable.chip_background),
1402                    r.getDrawable(R.drawable.chip_background_selected),
1403                    r.getDrawable(R.drawable.chip_background_invalid),
1404                    r.getDrawable(R.drawable.chip_delete),
1405                    def,
1406                    R.layout.more_item,
1407                    R.layout.chips_alternate_item,
1408                    r.getDimension(R.dimen.chip_height),
1409                    r.getDimension(R.dimen.chip_padding),
1410                    r.getDimension(R.dimen.chip_text_size));
1411        } else {
1412            mAddressAdapter = new EmailAddressAdapter(mActivity);
1413            list.setAdapter((EmailAddressAdapter)mAddressAdapter);
1414        }
1415        list.setTokenizer(new Rfc822Tokenizer());
1416        list.setValidator(mEmailValidator);
1417
1418        // NOTE: assumes no other filters are set
1419        list.setFilters(sRecipientFilters);
1420
1421        return list;
1422    }
1423
1424    /**
1425     * From com.google.android.gm.ComposeActivity Implements special address
1426     * cleanup rules: The first space key entry following an "@" symbol that is
1427     * followed by any combination of letters and symbols, including one+ dots
1428     * and zero commas, should insert an extra comma (followed by the space).
1429     */
1430    private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() };
1431
1432    private void setDate(TextView view, long millis) {
1433        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
1434                | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH
1435                | DateUtils.FORMAT_ABBREV_WEEKDAY;
1436
1437        // Unfortunately, DateUtils doesn't support a timezone other than the
1438        // default timezone provided by the system, so we have this ugly hack
1439        // here to trick it into formatting our time correctly. In order to
1440        // prevent all sorts of craziness, we synchronize on the TimeZone class
1441        // to prevent other threads from reading an incorrect timezone from
1442        // calls to TimeZone#getDefault()
1443        // TODO fix this if/when DateUtils allows for passing in a timezone
1444        String dateString;
1445        synchronized (TimeZone.class) {
1446            TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
1447            dateString = DateUtils.formatDateTime(mActivity, millis, flags);
1448            // setting the default back to null restores the correct behavior
1449            TimeZone.setDefault(null);
1450        }
1451        view.setText(dateString);
1452    }
1453
1454    private void setTime(TextView view, long millis) {
1455        int flags = DateUtils.FORMAT_SHOW_TIME;
1456        if (DateFormat.is24HourFormat(mActivity)) {
1457            flags |= DateUtils.FORMAT_24HOUR;
1458        }
1459
1460        // Unfortunately, DateUtils doesn't support a timezone other than the
1461        // default timezone provided by the system, so we have this ugly hack
1462        // here to trick it into formatting our time correctly. In order to
1463        // prevent all sorts of craziness, we synchronize on the TimeZone class
1464        // to prevent other threads from reading an incorrect timezone from
1465        // calls to TimeZone#getDefault()
1466        // TODO fix this if/when DateUtils allows for passing in a timezone
1467        String timeString;
1468        synchronized (TimeZone.class) {
1469            TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
1470            timeString = DateUtils.formatDateTime(mActivity, millis, flags);
1471            TimeZone.setDefault(null);
1472        }
1473        view.setText(timeString);
1474    }
1475
1476    private void setTimezone(int i) {
1477        if (i < 0 || i >= mTimezoneAdapter.getCount()) {
1478            return; // do nothing
1479        }
1480        TimezoneRow timezone = mTimezoneAdapter.getItem(i);
1481        mTimezoneTextView.setText(timezone.toString());
1482        mTimezoneButton.setText(timezone.toString());
1483        mTimezone = timezone.mId;
1484        mStartTime.timezone = mTimezone;
1485        mStartTime.normalize(true);
1486        mEndTime.timezone = mTimezone;
1487        mEndTime.normalize(true);
1488        mTimezoneAdapter.setCurrentTimezone(mTimezone);
1489    }
1490
1491    /**
1492     * @param isChecked
1493     */
1494    protected void setAllDayViewsVisibility(boolean isChecked) {
1495        if (isChecked) {
1496            if (mEndTime.hour == 0 && mEndTime.minute == 0) {
1497                mEndTime.monthDay--;
1498                long endMillis = mEndTime.normalize(true);
1499
1500                // Do not allow an event to have an end time
1501                // before the
1502                // start time.
1503                if (mEndTime.before(mStartTime)) {
1504                    mEndTime.set(mStartTime);
1505                    endMillis = mEndTime.normalize(true);
1506                }
1507                setDate(mEndDateButton, endMillis);
1508                setTime(mEndTimeButton, endMillis);
1509            }
1510
1511            mStartTimeButton.setVisibility(View.GONE);
1512            mEndTimeButton.setVisibility(View.GONE);
1513            mTimezoneButton.setVisibility(View.GONE);
1514            if (mTimezoneLabel != null) {
1515                mTimezoneLabel.setVisibility(View.GONE);
1516            }
1517        } else {
1518            if (mEndTime.hour == 0 && mEndTime.minute == 0) {
1519                mEndTime.monthDay++;
1520                long endMillis = mEndTime.normalize(true);
1521                setDate(mEndDateButton, endMillis);
1522                setTime(mEndTimeButton, endMillis);
1523            }
1524            mStartTimeButton.setVisibility(View.VISIBLE);
1525            mEndTimeButton.setVisibility(View.VISIBLE);
1526            mTimezoneButton.setVisibility(View.VISIBLE);
1527            if (mTimezoneLabel != null) {
1528                mTimezoneLabel.setVisibility(View.VISIBLE);
1529            }
1530        }
1531        updateHomeTime();
1532    }
1533
1534    @Override
1535    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
1536        // This is only used for the Calendar spinner in new events, and only fires when the
1537        // calendar selection changes.
1538        Cursor c = (Cursor) parent.getItemAtPosition(position);
1539        if (c == null) {
1540            // TODO: can this happen? should we drop this check?
1541            Log.w(TAG, "Cursor not set on calendar item");
1542            return;
1543        }
1544
1545        int colorColumn = c.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
1546        int color = c.getInt(colorColumn);
1547        int displayColor = Utils.getDisplayColorFromColor(color);
1548        mColorChip.setBackgroundColor(displayColor);
1549        mModel.mCalendarColor = color;
1550        mCalendarSelectorGroup.setBackgroundColor(displayColor);
1551
1552
1553        // Update the max/allowed reminders with the new calendar properties.
1554        int maxRemindersColumn = c.getColumnIndexOrThrow(Calendars.MAX_REMINDERS);
1555        mModel.mCalendarMaxReminders = c.getInt(maxRemindersColumn);
1556        int allowedRemindersColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_REMINDERS);
1557        mModel.mCalendarAllowedReminders = c.getString(allowedRemindersColumn);
1558
1559        // Discard the current reminders and replace them with the model's default reminder set.
1560        // We could attempt to save & restore the reminders that have been added, but that's
1561        // probably more trouble than it's worth.
1562        mModel.mReminders.clear();
1563        mModel.mReminders.addAll(mModel.mDefaultReminders);
1564        mModel.mHasAlarm = mModel.mReminders.size() != 0;
1565
1566        // Update the UI elements.
1567        mReminderItems.clear();
1568        LinearLayout reminderLayout =
1569            (LinearLayout) mScrollView.findViewById(R.id.reminder_items_container);
1570        reminderLayout.removeAllViews();
1571        prepareReminders();
1572    }
1573
1574    /**
1575     * Checks if the start and end times for this event should be displayed in
1576     * the Calendar app's time zone as well and formats and displays them.
1577     */
1578    private void updateHomeTime() {
1579        String tz = Utils.getTimeZone(mActivity, null);
1580        if (!mAllDayCheckBox.isChecked() && !TextUtils.equals(tz, mTimezone)
1581                && mModification != EditEventHelper.MODIFY_UNINITIALIZED) {
1582            int flags = DateUtils.FORMAT_SHOW_TIME;
1583            boolean is24Format = DateFormat.is24HourFormat(mActivity);
1584            if (is24Format) {
1585                flags |= DateUtils.FORMAT_24HOUR;
1586            }
1587            long millisStart = mStartTime.toMillis(false);
1588            long millisEnd = mEndTime.toMillis(false);
1589
1590            boolean isDSTStart = mStartTime.isDst != 0;
1591            boolean isDSTEnd = mEndTime.isDst != 0;
1592
1593            // First update the start date and times
1594            String tzDisplay = TimeZone.getTimeZone(tz).getDisplayName(
1595                    isDSTStart, TimeZone.SHORT, Locale.getDefault());
1596            StringBuilder time = new StringBuilder();
1597
1598            mSB.setLength(0);
1599            time.append(DateUtils
1600                    .formatDateRange(mActivity, mF, millisStart, millisStart, flags, tz))
1601                    .append(" ").append(tzDisplay);
1602            mStartTimeHome.setText(time.toString());
1603
1604            flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE
1605                    | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY;
1606            mSB.setLength(0);
1607            mStartDateHome
1608                    .setText(DateUtils.formatDateRange(
1609                            mActivity, mF, millisStart, millisStart, flags, tz).toString());
1610
1611            // Make any adjustments needed for the end times
1612            if (isDSTEnd != isDSTStart) {
1613                tzDisplay = TimeZone.getTimeZone(tz).getDisplayName(
1614                        isDSTEnd, TimeZone.SHORT, Locale.getDefault());
1615            }
1616            flags = DateUtils.FORMAT_SHOW_TIME;
1617            if (is24Format) {
1618                flags |= DateUtils.FORMAT_24HOUR;
1619            }
1620
1621            // Then update the end times
1622            time.setLength(0);
1623            mSB.setLength(0);
1624            time.append(DateUtils.formatDateRange(
1625                    mActivity, mF, millisEnd, millisEnd, flags, tz)).append(" ").append(tzDisplay);
1626            mEndTimeHome.setText(time.toString());
1627
1628            flags = DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE
1629                    | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY;
1630            mSB.setLength(0);
1631            mEndDateHome.setText(DateUtils.formatDateRange(
1632                            mActivity, mF, millisEnd, millisEnd, flags, tz).toString());
1633
1634            mStartHomeGroup.setVisibility(View.VISIBLE);
1635            mEndHomeGroup.setVisibility(View.VISIBLE);
1636        } else {
1637            mStartHomeGroup.setVisibility(View.GONE);
1638            mEndHomeGroup.setVisibility(View.GONE);
1639        }
1640    }
1641
1642    @Override
1643    public void onNothingSelected(AdapterView<?> parent) {
1644        mColorChip.setBackgroundColor(0);
1645
1646    }
1647}
1648