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