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