EditEventView.java revision dd95df57c8c5a58a85c4c0effad5652dec14f621
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.CalendarPreferenceActivity;
22import com.android.calendar.EmailAddressAdapter;
23import com.android.calendar.R;
24import com.android.calendar.TimezoneAdapter;
25import com.android.calendar.TimezoneAdapter.TimezoneRow;
26import com.android.calendar.Utils;
27import com.android.calendar.event.EditEventHelper.EditDoneRunnable;
28import com.android.common.Rfc822InputFilter;
29import com.android.common.Rfc822Validator;
30
31import android.app.Activity;
32import android.app.AlertDialog;
33import android.app.DatePickerDialog;
34import android.app.DatePickerDialog.OnDateSetListener;
35import android.app.ProgressDialog;
36import android.app.TimePickerDialog;
37import android.app.TimePickerDialog.OnTimeSetListener;
38import android.content.Context;
39import android.content.DialogInterface;
40import android.content.SharedPreferences;
41import android.content.res.Resources;
42import android.database.Cursor;
43import android.pim.EventRecurrence;
44import android.provider.Calendar.Calendars;
45import android.text.InputFilter;
46import android.text.TextUtils;
47import android.text.format.DateFormat;
48import android.text.format.DateUtils;
49import android.text.format.Time;
50import android.text.util.Rfc822Tokenizer;
51import android.view.LayoutInflater;
52import android.view.View;
53import android.widget.ArrayAdapter;
54import android.widget.Button;
55import android.widget.CheckBox;
56import android.widget.CompoundButton;
57import android.widget.DatePicker;
58import android.widget.ImageButton;
59import android.widget.LinearLayout;
60import android.widget.ListView;
61import android.widget.MultiAutoCompleteTextView;
62import android.widget.ResourceCursorAdapter;
63import android.widget.ScrollView;
64import android.widget.Spinner;
65import android.widget.TextView;
66import android.widget.TimePicker;
67
68import java.util.ArrayList;
69import java.util.Arrays;
70import java.util.Calendar;
71import java.util.HashMap;
72import java.util.TimeZone;
73
74public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener,
75        DialogInterface.OnClickListener {
76
77    private static final String TAG = EditEventView.class.getSimpleName();
78
79    private static final int REMINDER_FLING_VELOCITY = 2000;
80
81    private LayoutInflater mLayoutInflater;
82
83    TextView mLoadingMessage;
84    ScrollView mScrollView;
85    Button mStartDateButton;
86    Button mEndDateButton;
87    Button mStartTimeButton;
88    Button mEndTimeButton;
89    Button mSaveButton;
90    Button mDeleteButton;
91    Button mDiscardButton;
92    Button mTimezoneButton;
93    CheckBox mAllDayCheckBox;
94    Spinner mCalendarsSpinner;
95    Spinner mRepeatsSpinner;
96    Spinner mTransparencySpinner;
97    Spinner mVisibilitySpinner;
98    Spinner mResponseSpinner;
99    TextView mTitleTextView;
100    TextView mLocationTextView;
101    TextView mDescriptionTextView;
102    TextView mTimezoneTextView;
103    TextView mTimezoneFooterView;
104    View mRemindersSeparator;
105    LinearLayout mRemindersContainer;
106    MultiAutoCompleteTextView mAttendeesList;
107    ImageButton mAddAttendeesButton;
108    ListView mGuestList;
109    AttendeesAdapter mAttendeesAdapter;
110    AddAttendeeClickListener mAddAttendeesListener;
111
112    private ProgressDialog mLoadingCalendarsDialog;
113    private AlertDialog mNoCalendarsDialog;
114    private AlertDialog mTimezoneDialog;
115    private Activity mActivity;
116    private EditDoneRunnable mDone;
117    private View mView;
118    private CalendarEventModel mModel;
119    private Cursor mCalendarsCursor;
120    private EmailAddressAdapter mAddressAdapter;
121    private Rfc822Validator mEmailValidator;
122    private TimezoneAdapter mTimezoneAdapter;
123
124    private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer>(0);
125    private ArrayList<Integer> mReminderValues;
126    private ArrayList<String> mReminderLabels;
127    private int mDefaultReminderMinutes;
128
129    private boolean mSaveAfterQueryComplete = false;
130    private boolean mCalendarsCursorSet = false;
131
132    private Time mStartTime;
133    private Time mEndTime;
134    private String mTimezone;
135    private int mModification = EditEventHelper.MODIFY_UNINITIALIZED;
136
137    private EventRecurrence mEventRecurrence = new EventRecurrence();
138
139    private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
140
141    /* This class is used to update the time buttons. */
142    private class TimeListener implements OnTimeSetListener {
143        private View mView;
144
145        public TimeListener(View view) {
146            mView = view;
147        }
148
149        public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
150            // Cache the member variables locally to avoid inner class overhead.
151            Time startTime = mStartTime;
152            Time endTime = mEndTime;
153
154            // Cache the start and end millis so that we limit the number
155            // of calls to normalize() and toMillis(), which are fairly
156            // expensive.
157            long startMillis;
158            long endMillis;
159            if (mView == mStartTimeButton) {
160                // The start time was changed.
161                int hourDuration = endTime.hour - startTime.hour;
162                int minuteDuration = endTime.minute - startTime.minute;
163
164                startTime.hour = hourOfDay;
165                startTime.minute = minute;
166                startMillis = startTime.normalize(true);
167
168                // Also update the end time to keep the duration constant.
169                endTime.hour = hourOfDay + hourDuration;
170                endTime.minute = minute + minuteDuration;
171            } else {
172                // The end time was changed.
173                startMillis = startTime.toMillis(true);
174                endTime.hour = hourOfDay;
175                endTime.minute = minute;
176
177                // Move to the start time if the end time is before the start
178                // time.
179                if (endTime.before(startTime)) {
180                    endTime.monthDay = startTime.monthDay + 1;
181                }
182            }
183
184            endMillis = endTime.normalize(true);
185
186            setDate(mEndDateButton, endMillis);
187            setTime(mStartTimeButton, startMillis);
188            setTime(mEndTimeButton, endMillis);
189        }
190    }
191
192    private class AddAttendeeClickListener implements View.OnClickListener {
193        @Override
194        public void onClick(View v) {
195            mAttendeesList.performValidation();
196            mAttendeesAdapter.addAttendees(mAttendeesList.getText().toString());
197            mAttendeesList.setText("");
198        }
199    }
200
201    private class TimeClickListener implements View.OnClickListener {
202        private Time mTime;
203
204        public TimeClickListener(Time time) {
205            mTime = time;
206        }
207
208        public void onClick(View v) {
209            new TimePickerDialog(mActivity, new TimeListener(v), mTime.hour, mTime.minute,
210                    DateFormat.is24HourFormat(mActivity)).show();
211        }
212    }
213
214    private class DateListener implements OnDateSetListener {
215        View mView;
216
217        public DateListener(View view) {
218            mView = view;
219        }
220
221        public void onDateSet(DatePicker view, int year, int month, int monthDay) {
222            // Cache the member variables locally to avoid inner class overhead.
223            Time startTime = mStartTime;
224            Time endTime = mEndTime;
225
226            // Cache the start and end millis so that we limit the number
227            // of calls to normalize() and toMillis(), which are fairly
228            // expensive.
229            long startMillis;
230            long endMillis;
231            if (mView == mStartDateButton) {
232                // The start date was changed.
233                int yearDuration = endTime.year - startTime.year;
234                int monthDuration = endTime.month - startTime.month;
235                int monthDayDuration = endTime.monthDay - startTime.monthDay;
236
237                startTime.year = year;
238                startTime.month = month;
239                startTime.monthDay = monthDay;
240                startMillis = startTime.normalize(true);
241
242                // Also update the end date to keep the duration constant.
243                endTime.year = year + yearDuration;
244                endTime.month = month + monthDuration;
245                endTime.monthDay = monthDay + monthDayDuration;
246                endMillis = endTime.normalize(true);
247
248                // If the start date has changed then update the repeats.
249                populateRepeats();
250            } else {
251                // The end date was changed.
252                startMillis = startTime.toMillis(true);
253                endTime.year = year;
254                endTime.month = month;
255                endTime.monthDay = monthDay;
256                endMillis = endTime.normalize(true);
257
258                // Do not allow an event to have an end time before the start
259                // time.
260                if (endTime.before(startTime)) {
261                    endTime.set(startTime);
262                    endMillis = startMillis;
263                }
264            }
265
266            setDate(mStartDateButton, startMillis);
267            setDate(mEndDateButton, endMillis);
268            setTime(mEndTimeButton, endMillis); // In case end time had to be
269            // reset
270        }
271    }
272
273    // Fills in the date and time fields
274    private void populateWhen() {
275        long startMillis = mStartTime.toMillis(false /* use isDst */);
276        long endMillis = mEndTime.toMillis(false /* use isDst */);
277        setDate(mStartDateButton, startMillis);
278        setDate(mEndDateButton, endMillis);
279
280        setTime(mStartTimeButton, startMillis);
281        setTime(mEndTimeButton, endMillis);
282
283        mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
284        mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
285
286        mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
287        mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
288    }
289
290    private void populateTimezone() {
291        mTimezoneButton.setOnClickListener(new View.OnClickListener() {
292            @Override
293            public void onClick(View v) {
294                showTimezoneDialog();
295            }
296        });
297        setTimezone(mTimezoneAdapter.getRowById(mTimezone));
298    }
299
300    private void showTimezoneDialog() {
301        mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
302        AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
303        builder.setTitle(R.string.timezone_label);
304        builder.setSingleChoiceItems(
305                mTimezoneAdapter, mTimezoneAdapter.getRowById(mTimezone), this);
306        mTimezoneDialog = builder.create();
307        mTimezoneFooterView.setText(
308                mActivity.getString(R.string.edit_event_show_all) + " >");
309        mTimezoneFooterView.setOnClickListener(new View.OnClickListener() {
310            @Override
311            public void onClick(View v) {
312                mTimezoneDialog.getListView().removeFooterView(mTimezoneFooterView);
313                mTimezoneAdapter.showAllTimezones();
314                final int row = mTimezoneAdapter.getRowById(mTimezone);
315                // we need to post the selection changes to have them have
316                // any effect
317                mTimezoneDialog.getListView().post(new Runnable() {
318                    @Override
319                    public void run() {
320                        mTimezoneDialog.getListView().setItemChecked(row, true);
321                        mTimezoneDialog.getListView().setSelection(row);
322                    }
323                });
324            }
325        });
326        mTimezoneDialog.getListView().addFooterView(mTimezoneFooterView);
327        mTimezoneDialog.show();
328    }
329
330    private void populateRepeats() {
331        Time time = mStartTime;
332        Resources r = mActivity.getResources();
333        int resource = android.R.layout.simple_spinner_item;
334
335        String[] days = new String[] {
336                DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
337                DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
338                DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
339                DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
340                DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
341                DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
342                DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
343        };
344        String[] ordinals = r.getStringArray(R.array.ordinal_labels);
345
346        // Only display "Custom" in the spinner if the device does not support
347        // the recurrence functionality of the event. Only display every weekday
348        // if the event starts on a weekday.
349        boolean isCustomRecurrence = isCustomRecurrence();
350        boolean isWeekdayEvent = isWeekdayEvent();
351
352        ArrayList<String> repeatArray = new ArrayList<String>(0);
353        ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
354
355        repeatArray.add(r.getString(R.string.does_not_repeat));
356        recurrenceIndexes.add(EditEventHelper.DOES_NOT_REPEAT);
357
358        repeatArray.add(r.getString(R.string.daily));
359        recurrenceIndexes.add(EditEventHelper.REPEATS_DAILY);
360
361        if (isWeekdayEvent) {
362            repeatArray.add(r.getString(R.string.every_weekday));
363            recurrenceIndexes.add(EditEventHelper.REPEATS_EVERY_WEEKDAY);
364        }
365
366        String format = r.getString(R.string.weekly);
367        repeatArray.add(String.format(format, time.format("%A")));
368        recurrenceIndexes.add(EditEventHelper.REPEATS_WEEKLY_ON_DAY);
369
370        // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance
371        // of the given day.
372        int dayNumber = (time.monthDay - 1) / 7;
373        format = r.getString(R.string.monthly_on_day_count);
374        repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
375        recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT);
376
377        format = r.getString(R.string.monthly_on_day);
378        repeatArray.add(String.format(format, time.monthDay));
379        recurrenceIndexes.add(EditEventHelper.REPEATS_MONTHLY_ON_DAY);
380
381        long when = time.toMillis(false);
382        format = r.getString(R.string.yearly);
383        int flags = 0;
384        if (DateFormat.is24HourFormat(mActivity)) {
385            flags |= DateUtils.FORMAT_24HOUR;
386        }
387        repeatArray.add(String.format(format, DateUtils.formatDateTime(mActivity, when, flags)));
388        recurrenceIndexes.add(EditEventHelper.REPEATS_YEARLY);
389
390        if (isCustomRecurrence) {
391            repeatArray.add(r.getString(R.string.custom));
392            recurrenceIndexes.add(EditEventHelper.REPEATS_CUSTOM);
393        }
394        mRecurrenceIndexes = recurrenceIndexes;
395
396        int position = recurrenceIndexes.indexOf(EditEventHelper.DOES_NOT_REPEAT);
397        if (mModel.mRrule != null) {
398            if (isCustomRecurrence) {
399                position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_CUSTOM);
400            } else {
401                switch (mEventRecurrence.freq) {
402                    case EventRecurrence.DAILY:
403                        position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_DAILY);
404                        break;
405                    case EventRecurrence.WEEKLY:
406                        if (mEventRecurrence.repeatsOnEveryWeekDay()) {
407                            position = recurrenceIndexes
408                                    .indexOf(EditEventHelper.REPEATS_EVERY_WEEKDAY);
409                        } else {
410                            position = recurrenceIndexes
411                                    .indexOf(EditEventHelper.REPEATS_WEEKLY_ON_DAY);
412                        }
413                        break;
414                    case EventRecurrence.MONTHLY:
415                        if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
416                            position = recurrenceIndexes
417                                    .indexOf(EditEventHelper.REPEATS_MONTHLY_ON_DAY_COUNT);
418                        } else {
419                            position = recurrenceIndexes
420                                    .indexOf(EditEventHelper.REPEATS_MONTHLY_ON_DAY);
421                        }
422                        break;
423                    case EventRecurrence.YEARLY:
424                        position = recurrenceIndexes.indexOf(EditEventHelper.REPEATS_YEARLY);
425                        break;
426                }
427            }
428        }
429        ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity, resource, repeatArray);
430        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
431        mRepeatsSpinner.setAdapter(adapter);
432        mRepeatsSpinner.setSelection(position);
433
434        // Don't allow the user to make exceptions recurring events.
435        if (mModel.mOriginalEvent != null) {
436            mRepeatsSpinner.setEnabled(false);
437        }
438    }
439
440    private boolean isCustomRecurrence() {
441
442        if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
443            return true;
444        }
445
446        if (mEventRecurrence.freq == 0) {
447            return false;
448        }
449
450        switch (mEventRecurrence.freq) {
451            case EventRecurrence.DAILY:
452                return false;
453            case EventRecurrence.WEEKLY:
454                if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
455                    return false;
456                } else if (mEventRecurrence.bydayCount == 1) {
457                    return false;
458                }
459                break;
460            case EventRecurrence.MONTHLY:
461                if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
462                    return false;
463                } else if (mEventRecurrence.bydayCount == 0
464                        && mEventRecurrence.bymonthdayCount == 1) {
465                    return false;
466                }
467                break;
468            case EventRecurrence.YEARLY:
469                return false;
470        }
471
472        return true;
473    }
474
475    private boolean isWeekdayEvent() {
476        if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {
477            return true;
478        }
479        return false;
480    }
481
482    private class DateClickListener implements View.OnClickListener {
483        private Time mTime;
484
485        public DateClickListener(Time time) {
486            mTime = time;
487        }
488
489        public void onClick(View v) {
490            new DatePickerDialog(mActivity, new DateListener(v), mTime.year, mTime.month,
491                    mTime.monthDay).show();
492        }
493    }
494
495    static private class CalendarsAdapter extends ResourceCursorAdapter {
496        public CalendarsAdapter(Context context, Cursor c) {
497            super(context, R.layout.calendars_item, c);
498            setDropDownViewResource(R.layout.calendars_dropdown_item);
499        }
500
501        @Override
502        public void bindView(View view, Context context, Cursor cursor) {
503            View colorBar = view.findViewById(R.id.color);
504            int colorColumn = cursor.getColumnIndexOrThrow(Calendars.COLOR);
505            int nameColumn = cursor.getColumnIndexOrThrow(Calendars.DISPLAY_NAME);
506            int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
507            if (colorBar != null) {
508                colorBar.setBackgroundDrawable(Utils.getColorChip(cursor.getInt(colorColumn)));
509            }
510
511            TextView name = (TextView) view.findViewById(R.id.calendar_name);
512            if (name != null) {
513                String displayName = cursor.getString(nameColumn);
514                name.setText(displayName);
515                name.setTextColor(0xFF000000);
516
517                TextView accountName = (TextView) view.findViewById(R.id.account_name);
518                if (accountName != null) {
519                    Resources res = context.getResources();
520                    accountName.setText(cursor.getString(ownerColumn));
521                    accountName.setVisibility(TextView.VISIBLE);
522                    accountName.setTextColor(res.getColor(R.color.calendar_owner_text_color));
523                }
524            }
525        }
526    }
527
528    // This is called if the user clicks on one of the buttons: "Save",
529    // "Discard", or "Delete". This is also called if the user clicks
530    // on the "remove reminder" button.
531    public void onClick(View v) {
532        if (v == mSaveButton) {
533            // If we're creating a new event but haven't gotten any calendars
534            // yet let the user know we're waiting for calendars to finish
535            // loading. The save button isn't enabled until we have a non-null
536            // mModel.
537            mAddAttendeesListener.onClick(v);
538            if (mCalendarsCursor == null && mModel.mUri == null) {
539                if (mLoadingCalendarsDialog == null) {
540                    // Create the progress dialog
541                    mLoadingCalendarsDialog = ProgressDialog.show(mActivity,
542                            mActivity.getText(R.string.loading_calendars_title),
543                            mActivity.getText(R.string.loading_calendars_message), true, true,
544                            this);
545                    mSaveAfterQueryComplete = true;
546                }
547            } else if (fillModelFromUI()) {
548                mDone.setDoneCode(Utils.DONE_SAVE);
549                mDone.run();
550            } else {
551                mDone.setDoneCode(Utils.DONE_REVERT);
552                mDone.run();
553            }
554            return;
555        }
556
557        if (v == mDeleteButton) {
558            mDone.setDoneCode(Utils.DONE_DELETE);
559            mDone.run();
560            return;
561        }
562
563        if (v == mDiscardButton) {
564            mDone.setDoneCode(Utils.DONE_REVERT);
565            mDone.run();
566            return;
567        }
568
569        // This must be a click on one of the "remove reminder" buttons
570        LinearLayout reminderItem = (LinearLayout) v.getParent();
571        LinearLayout parent = (LinearLayout) reminderItem.getParent();
572        parent.removeView(reminderItem);
573        mReminderItems.remove(reminderItem);
574        updateRemindersVisibility(mReminderItems.size());
575    }
576
577    // This is called if the user cancels the "No calendars" dialog.
578    // The "No calendars" dialog is shown if there are no syncable calendars.
579    public void onCancel(DialogInterface dialog) {
580        if (dialog == mLoadingCalendarsDialog) {
581            mLoadingCalendarsDialog = null;
582            mSaveAfterQueryComplete = false;
583        } else if (dialog == mNoCalendarsDialog) {
584            mDone.setDoneCode(Utils.DONE_REVERT);
585            mDone.run();
586            return;
587        }
588    }
589
590    // This is called if the user clicks on a dialog button.
591    public void onClick(DialogInterface dialog, int which) {
592        if (dialog == mNoCalendarsDialog) {
593            mDone.setDoneCode(Utils.DONE_REVERT);
594            mDone.run();
595        } else if (dialog == mTimezoneDialog) {
596            if (which >= 0 && which < mTimezoneAdapter.getCount()) {
597                setTimezone(which);
598                dialog.dismiss();
599            }
600        }
601    }
602
603    // Goes through the UI elements and updates the model as necessary
604    public boolean fillModelFromUI() {
605        if (mModel == null) {
606            return false;
607        }
608        mModel.mReminderMinutes = EventViewUtils.reminderItemsToMinutes(mReminderItems,
609                mReminderValues);
610        mModel.mHasAlarm = mReminderItems.size() > 0;
611        mModel.mTitle = mTitleTextView.getText().toString().trim();
612        mModel.mAllDay = mAllDayCheckBox.isChecked();
613        mModel.mLocation = mLocationTextView.getText().toString().trim();
614        mModel.mDescription = mDescriptionTextView.getText().toString().trim();
615        int position = mResponseSpinner.getSelectedItemPosition();
616        if (position > 0) {
617            mModel.mSelfAttendeeStatus = EditEventHelper.ATTENDEE_VALUES[position];
618        }
619
620        if (mGuestList != null) {
621            AttendeesAdapter adapter = (AttendeesAdapter) mGuestList.getAdapter();
622            if (adapter != null && !adapter.isEmpty()) {
623                int size = adapter.getCount();
624                mModel.mAttendeesList.clear();
625                for (int i = 0; i < size; i++) {
626                    Attendee attendee = adapter.getItem(i);
627                    if (attendee == null || adapter.isRemoved(i)) {
628                        continue;
629                    }
630                    mModel.addAttendee(attendee);
631                }
632            }
633        }
634
635        // If this was a new event we need to fill in the Calendar information
636        if (mModel.mUri == null) {
637            mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId();
638            int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
639            if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
640                String defaultCalendar = mCalendarsCursor
641                        .getString(EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
642                Utils.setSharedPreference(mActivity,
643                        CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, defaultCalendar);
644                mModel.mOwnerAccount = defaultCalendar;
645                mModel.mOrganizer = defaultCalendar;
646                mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID);
647            }
648        }
649
650        if (mModel.mAllDay) {
651            // Reset start and end time, increment the monthDay by 1, and set
652            // the timezone to UTC, as required for all-day events.
653            mTimezone = Time.TIMEZONE_UTC;
654            mStartTime.hour = 0;
655            mStartTime.minute = 0;
656            mStartTime.second = 0;
657            mStartTime.timezone = mTimezone;
658            mModel.mStart = mStartTime.normalize(true);
659
660            // Round up to the next day
661            if (mEndTime.hour > 0 || mEndTime.minute > 0 || mEndTime.second > 0
662                    || mEndTime.monthDay == mStartTime.monthDay) {
663                mEndTime.monthDay++;
664            }
665            mEndTime.hour = 0;
666            mEndTime.minute = 0;
667            mEndTime.second = 0;
668            mEndTime.timezone = mTimezone;
669            mModel.mEnd = mEndTime.normalize(true);
670        } else {
671            mStartTime.timezone = mTimezone;
672            mEndTime.timezone = mTimezone;
673            mModel.mStart = mStartTime.toMillis(true);
674            mModel.mEnd = mEndTime.toMillis(true);
675        }
676        mModel.mTimezone = mTimezone;
677        mModel.mVisibility = mVisibilitySpinner.getSelectedItemPosition();
678        mModel.mTransparency = mTransparencySpinner.getSelectedItemPosition() != 0;
679
680        int selection;
681        // If we're making an exception we don't want it to be a repeating
682        // event.
683        if (mModification == EditEventHelper.MODIFY_SELECTED) {
684            selection = EditEventHelper.DOES_NOT_REPEAT;
685        } else {
686            position = mRepeatsSpinner.getSelectedItemPosition();
687            selection = mRecurrenceIndexes.get(position);
688        }
689
690        EditEventHelper.updateRecurrenceRule(selection, mModel,
691                Utils.getFirstDayOfWeek(mActivity) + 1);
692
693        // Save the timezone so we can display it as a standard option next time
694        if (!mModel.mAllDay) {
695            mTimezoneAdapter.saveRecentTimezone(mTimezone);
696        }
697        return true;
698    }
699
700    public EditEventView(Activity activity, View view, EditDoneRunnable done) {
701
702        mActivity = activity;
703        mView = view;
704        mDone = done;
705
706        // cache top level view elements
707        mLoadingMessage = (TextView) view.findViewById(R.id.loading_message);
708        mScrollView = (ScrollView) view.findViewById(R.id.scroll_view);
709
710        mLayoutInflater = activity.getLayoutInflater();
711
712        // cache all the widgets
713        mTitleTextView = (TextView) view.findViewById(R.id.title);
714        mLocationTextView = (TextView) view.findViewById(R.id.location);
715        mDescriptionTextView = (TextView) view.findViewById(R.id.description);
716        mTimezoneTextView = (TextView) view.findViewById(R.id.timezone_label);
717        mTimezoneFooterView = (TextView) mLayoutInflater.inflate(R.layout.timezone_footer, null);
718        mStartDateButton = (Button) view.findViewById(R.id.start_date);
719        mEndDateButton = (Button) view.findViewById(R.id.end_date);
720        mStartTimeButton = (Button) view.findViewById(R.id.start_time);
721        mEndTimeButton = (Button) view.findViewById(R.id.end_time);
722        mTimezoneButton = (Button) view.findViewById(R.id.timezone);
723        mAllDayCheckBox = (CheckBox) view.findViewById(R.id.is_all_day);
724        mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars);
725        mRepeatsSpinner = (Spinner) view.findViewById(R.id.repeats);
726        mTransparencySpinner = (Spinner) view.findViewById(R.id.availability);
727        mVisibilitySpinner = (Spinner) view.findViewById(R.id.visibility);
728        mResponseSpinner = (Spinner) view.findViewById(R.id.response_value);
729        mRemindersSeparator = view.findViewById(R.id.reminders_separator);
730        mRemindersContainer = (LinearLayout) view.findViewById(R.id.reminder_items_container);
731
732        mSaveButton = (Button) view.findViewById(R.id.save);
733        mDeleteButton = (Button) view.findViewById(R.id.delete);
734
735        mDiscardButton = (Button) view.findViewById(R.id.discard);
736        mDiscardButton.setOnClickListener(this);
737
738        mAddAttendeesButton = (ImageButton) view.findViewById(R.id.attendee_add);
739        mAddAttendeesListener = new AddAttendeeClickListener();
740        mAddAttendeesButton.setOnClickListener(mAddAttendeesListener);
741
742        mStartTime = new Time();
743        mEndTime = new Time();
744        mTimezone = TimeZone.getDefault().getID();
745        mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
746
747        mGuestList = (ListView) mView.findViewById(R.id.attendee_list);
748
749        // Display loading screen
750        setModel(null);
751    }
752
753    /**
754     * Fill in the view with the contents of the given event model. This allows
755     * an edit view to be initialized before the event has been loaded. Passing
756     * in null for the model will display a loading screen. A non-null model
757     * will fill in the view's fields with the data contained in the model.
758     *
759     * @param model The event model to pull the data from
760     */
761    public void setModel(CalendarEventModel model) {
762        mModel = model;
763
764        // Need to close the autocomplete adapter to prevent leaking cursors.
765        if (mAddressAdapter != null) {
766            mAddressAdapter.close();
767            mAddressAdapter = null;
768        }
769
770        if (model == null) {
771            // Display loading screen
772            mLoadingMessage.setVisibility(View.VISIBLE);
773            mScrollView.setVisibility(View.GONE);
774            mSaveButton.setEnabled(false);
775            mDeleteButton.setEnabled(false);
776            return;
777        }
778
779        long begin = model.mStart;
780        long end = model.mEnd;
781        mTimezone = model.mTimezone; // this will be UTC for all day events
782
783        // Set up the starting times
784        if (begin > 0) {
785            mStartTime.timezone = mTimezone;
786            mStartTime.set(begin);
787            mStartTime.normalize(true);
788        }
789        if (end > 0) {
790            mEndTime.timezone = mTimezone;
791            mEndTime.set(end);
792            mEndTime.normalize(true);
793        }
794        String rrule = model.mRrule;
795        if (rrule != null) {
796            mEventRecurrence.parse(rrule);
797        }
798
799        // If the user is allowed to change the attendees set up the view and
800        // validator
801        if (model.mHasAttendeeData) {
802            String domain = "gmail.com";
803            if (!TextUtils.isEmpty(model.mOwnerAccount)) {
804                String ownerDomain = EditEventHelper.extractDomain(model.mOwnerAccount);
805                if (!TextUtils.isEmpty(ownerDomain)) {
806                    domain = ownerDomain;
807                }
808            }
809            mAddressAdapter = new EmailAddressAdapter(mActivity);
810            mEmailValidator = new Rfc822Validator(domain);
811            mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees);
812        } else {
813            View attGroup = mView.findViewById(R.id.attendees_group);
814            attGroup.setVisibility(View.GONE);
815        }
816
817        mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
818            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
819                if (isChecked) {
820                    if (mEndTime.hour == 0 && mEndTime.minute == 0) {
821                        mEndTime.monthDay--;
822                        long endMillis = mEndTime.normalize(true);
823
824                        // Do not allow an event to have an end time before the
825                        // start time.
826                        if (mEndTime.before(mStartTime)) {
827                            mEndTime.set(mStartTime);
828                            endMillis = mEndTime.normalize(true);
829                        }
830                        setDate(mEndDateButton, endMillis);
831                        setTime(mEndTimeButton, endMillis);
832                    }
833
834                    mStartTimeButton.setVisibility(View.GONE);
835                    mEndTimeButton.setVisibility(View.GONE);
836                    mTimezoneButton.setVisibility(View.GONE);
837                    mTimezoneTextView.setVisibility(View.GONE);
838                } else {
839                    if (mEndTime.hour == 0 && mEndTime.minute == 0) {
840                        mEndTime.monthDay++;
841                        long endMillis = mEndTime.normalize(true);
842                        setDate(mEndDateButton, endMillis);
843                        setTime(mEndTimeButton, endMillis);
844                    }
845                    mStartTimeButton.setVisibility(View.VISIBLE);
846                    mEndTimeButton.setVisibility(View.VISIBLE);
847                    mTimezoneButton.setVisibility(View.VISIBLE);
848                    mTimezoneTextView.setVisibility(View.VISIBLE);
849                }
850            }
851        });
852
853        if (model.mAllDay) {
854            mAllDayCheckBox.setChecked(true);
855            // put things back in local time for all day events
856            mTimezone = TimeZone.getDefault().getID();
857            mStartTime.timezone = mTimezone;
858            mStartTime.normalize(true);
859            mEndTime.timezone = mTimezone;
860            mEndTime.normalize(true);
861        } else {
862            mAllDayCheckBox.setChecked(false);
863        }
864
865        mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
866        if (mTimezoneDialog != null) {
867            mTimezoneDialog.getListView().setAdapter(mTimezoneAdapter);
868        }
869
870        mSaveButton.setOnClickListener(this);
871        mDeleteButton.setOnClickListener(this);
872        mSaveButton.setEnabled(true);
873        mDeleteButton.setEnabled(true);
874
875        // Initialize the reminder values array.
876        Resources r = mActivity.getResources();
877        String[] strings = r.getStringArray(R.array.reminder_minutes_values);
878        int size = strings.length;
879        ArrayList<Integer> list = new ArrayList<Integer>(size);
880        for (int i = 0; i < size; i++) {
881            list.add(Integer.parseInt(strings[i]));
882        }
883        mReminderValues = list;
884        String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
885        mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
886
887        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(mActivity);
888        String durationString = prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER,
889                "0");
890        mDefaultReminderMinutes = Integer.parseInt(durationString);
891
892        int numReminders = 0;
893        if (model.mHasAlarm) {
894            ArrayList<Integer> minutes = model.mReminderMinutes;
895            numReminders = minutes.size();
896            for (Integer minute : minutes) {
897                EventViewUtils.addMinutesToList( mActivity, mReminderValues, mReminderLabels,
898                        minute);
899                EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
900                        mReminderValues, mReminderLabels, minute);
901            }
902        }
903        updateRemindersVisibility(numReminders);
904
905        // Setup the + Add Reminder Button
906        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
907            public void onClick(View v) {
908                addReminder();
909            }
910        };
911        ImageButton reminderRemoveButton = (ImageButton) mView.findViewById(R.id.reminder_add);
912        reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
913
914        mTitleTextView.setText(model.mTitle);
915        mLocationTextView.setText(model.mLocation);
916        mDescriptionTextView.setText(model.mDescription);
917        mTransparencySpinner.setSelection(model.mTransparency ? 1 : 0);
918        mVisibilitySpinner.setSelection(model.mVisibility);
919        mResponseSpinner.setSelection(findResponseIndexFor(model.mSelfAttendeeStatus));
920
921        if (model.mUri != null) {
922            // This is an existing event so hide the calendar spinner
923            // since we can't change the calendar.
924            View calendarGroup = mView.findViewById(R.id.calendar_group);
925            calendarGroup.setVisibility(View.GONE);
926        } else {
927            mDeleteButton.setVisibility(View.GONE);
928        }
929
930        populateWhen();
931        populateTimezone();
932        populateRepeats();
933        updateAttendees(model.mAttendeesList);
934        mScrollView.setVisibility(View.VISIBLE);
935        mLoadingMessage.setVisibility(View.GONE);
936    }
937
938    private int findResponseIndexFor(int response) {
939        int size = EditEventHelper.ATTENDEE_VALUES.length;
940        for (int index = 0; index < size; index++) {
941            if (EditEventHelper.ATTENDEE_VALUES[index] == response) {
942                return index;
943            }
944        }
945        return 0;
946    }
947
948    public void setCalendarsCursor(Cursor cursor) {
949        // If there are no syncable calendars, then we cannot allow
950        // creating a new event.
951        mCalendarsCursor = cursor;
952        if (cursor == null || cursor.getCount() == 0) {
953            // Cancel the "loading calendars" dialog if it exists
954            if (mSaveAfterQueryComplete) {
955                mLoadingCalendarsDialog.cancel();
956            }
957            // Create an error message for the user that, when clicked,
958            // will exit this activity without saving the event.
959            AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
960            builder.setTitle(R.string.no_syncable_calendars).setIcon(
961                    android.R.drawable.ic_dialog_alert).setMessage(R.string.no_calendars_found)
962                    .setPositiveButton(android.R.string.ok, this).setOnCancelListener(this);
963            mNoCalendarsDialog = builder.show();
964            return;
965        }
966
967        int defaultCalendarPosition = findDefaultCalendarPosition(cursor);
968
969        // populate the calendars spinner
970        CalendarsAdapter adapter = new CalendarsAdapter(mActivity, cursor);
971        mCalendarsSpinner.setAdapter(adapter);
972        mCalendarsSpinner.setSelection(defaultCalendarPosition);
973        mCalendarsCursorSet = true;
974
975        // Find user domain and set it to the validator.
976        // TODO: we may want to update this validator if the user actually picks
977        // a different calendar. maybe not. depends on what we want for the
978        // user experience. this may change when we add support for multiple
979        // accounts, anyway.
980        if (mModel != null && mModel.mHasAttendeeData
981                && cursor.moveToPosition(defaultCalendarPosition)) {
982            String ownEmail = cursor.getString(EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
983            if (ownEmail != null) {
984                String domain = EditEventHelper.extractDomain(ownEmail);
985                if (domain != null) {
986                    mEmailValidator = new Rfc822Validator(domain);
987                    mAttendeesList.setValidator(mEmailValidator);
988                }
989            }
990        }
991        if (mSaveAfterQueryComplete) {
992            mLoadingCalendarsDialog.cancel();
993            if (fillModelFromUI()) {
994                mDone.setDoneCode(Utils.DONE_SAVE);
995                mDone.run();
996            } else {
997                mDone.setDoneCode(Utils.DONE_REVERT);
998                mDone.run();
999            }
1000            return;
1001        }
1002    }
1003
1004    public void setModification(int modifyWhich) {
1005        mModification = modifyWhich;
1006        // If we are modifying all the events in a
1007        // series then disable and ignore the date.
1008        if (modifyWhich == Utils.MODIFY_ALL) {
1009            mStartDateButton.setEnabled(false);
1010            mEndDateButton.setEnabled(false);
1011        } else if (modifyWhich == Utils.MODIFY_SELECTED) {
1012            mRepeatsSpinner.setEnabled(false);
1013        }
1014    }
1015
1016    // Find the calendar position in the cursor that matches calendar in
1017    // preference
1018    private int findDefaultCalendarPosition(Cursor calendarsCursor) {
1019        if (calendarsCursor.getCount() <= 0) {
1020            return -1;
1021        }
1022
1023        String defaultCalendar = Utils.getSharedPreference(mActivity,
1024                CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, null);
1025
1026        if (defaultCalendar == null) {
1027            return 0;
1028        }
1029        int calendarsOwnerColumn = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
1030        int position = 0;
1031        calendarsCursor.moveToPosition(-1);
1032        while (calendarsCursor.moveToNext()) {
1033            if (defaultCalendar.equals(calendarsCursor.getString(calendarsOwnerColumn))) {
1034                return position;
1035            }
1036            position++;
1037        }
1038        return 0;
1039    }
1040
1041    public void updateAttendees(HashMap<String, Attendee> attendeesList) {
1042        if (mAttendeesAdapter == null) {
1043            mAttendeesAdapter = new AttendeesAdapter(mActivity, mEmailValidator);
1044        }
1045        if (attendeesList.size() > 0) {
1046            mAttendeesAdapter.addAttendees(attendeesList);
1047            mGuestList.setAdapter(mAttendeesAdapter);
1048        }
1049    }
1050
1051    private void updateRemindersVisibility(int numReminders) {
1052        if (numReminders == 0) {
1053            mRemindersSeparator.setVisibility(View.GONE);
1054            mRemindersContainer.setVisibility(View.GONE);
1055        } else {
1056            mRemindersSeparator.setVisibility(View.VISIBLE);
1057            mRemindersContainer.setVisibility(View.VISIBLE);
1058        }
1059    }
1060
1061    public void addReminder() {
1062        // TODO: when adding a new reminder, make it different from the
1063        // last one in the list (if any).
1064        if (mDefaultReminderMinutes == 0) {
1065            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
1066                    mReminderValues, mReminderLabels, 10 /* minutes */);
1067        } else {
1068            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
1069                    mReminderValues, mReminderLabels, mDefaultReminderMinutes);
1070        }
1071        updateRemindersVisibility(mReminderItems.size());
1072        mScrollView.fling(REMINDER_FLING_VELOCITY);
1073    }
1074
1075    public int getReminderCount() {
1076        return mReminderItems.size();
1077    }
1078
1079    // From com.google.android.gm.ComposeActivity
1080    private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) {
1081        MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) mView.findViewById(res);
1082        list.setAdapter(mAddressAdapter);
1083        list.setTokenizer(new Rfc822Tokenizer());
1084        list.setValidator(mEmailValidator);
1085
1086        // NOTE: assumes no other filters are set
1087        list.setFilters(sRecipientFilters);
1088
1089        return list;
1090    }
1091
1092    /**
1093     * From com.google.android.gm.ComposeActivity Implements special address
1094     * cleanup rules: The first space key entry following an "@" symbol that is
1095     * followed by any combination of letters and symbols, including one+ dots
1096     * and zero commas, should insert an extra comma (followed by the space).
1097     */
1098    private static InputFilter[] sRecipientFilters = new InputFilter[] {
1099        new Rfc822InputFilter()
1100    };
1101
1102    private void setDate(TextView view, long millis) {
1103        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
1104                | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH
1105                | DateUtils.FORMAT_ABBREV_WEEKDAY;
1106
1107        // Unfortunately, DateUtils doesn't support a timezone other than the
1108        // default timezone provided by the system, so we have this ugly hack
1109        // here to trick it into formatting our time correctly. In order to
1110        // prevent all sorts of craziness, we synchronize on the TimeZone class
1111        // to prevent other threads from reading an incorrect timezone from
1112        // calls to TimeZone#getDefault()
1113        // TODO fix this if/when DateUtils allows for passing in a timezone
1114        String dateString;
1115        synchronized (TimeZone.class) {
1116            TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
1117            dateString = DateUtils.formatDateTime(mActivity, millis, flags);
1118            // setting the default back to null restores the correct behavior
1119            TimeZone.setDefault(null);
1120        }
1121        view.setText(dateString);
1122    }
1123
1124    private void setTime(TextView view, long millis) {
1125        int flags = DateUtils.FORMAT_SHOW_TIME;
1126        if (DateFormat.is24HourFormat(mActivity)) {
1127            flags |= DateUtils.FORMAT_24HOUR;
1128        }
1129
1130        // Unfortunately, DateUtils doesn't support a timezone other than the
1131        // default timezone provided by the system, so we have this ugly hack
1132        // here to trick it into formatting our time correctly. In order to
1133        // prevent all sorts of craziness, we synchronize on the TimeZone class
1134        // to prevent other threads from reading an incorrect timezone from
1135        // calls to TimeZone#getDefault()
1136        // TODO fix this if/when DateUtils allows for passing in a timezone
1137        String timeString;
1138        synchronized (TimeZone.class) {
1139            TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
1140            timeString = DateUtils.formatDateTime(mActivity, millis, flags);
1141            TimeZone.setDefault(null);
1142        }
1143        view.setText(timeString);
1144    }
1145
1146    private void setTimezone(int i) {
1147        if (i < 0 || i >= mTimezoneAdapter.getCount()) {
1148            return; // do nothing
1149        }
1150        TimezoneRow timezone = mTimezoneAdapter.getItem(i);
1151        mTimezoneButton.setText(timezone.toString());
1152        mTimezone = timezone.mId;
1153        mStartTime.timezone = mTimezone;
1154        mStartTime.normalize(true);
1155        mEndTime.timezone = mTimezone;
1156        mEndTime.normalize(true);
1157        mTimezoneAdapter.setCurrentTimezone(mTimezone);
1158    }
1159}
1160