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