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