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