EditEventView.java revision bdcb9fcc732d52708fafb068f535a2c93ff2356f
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calendar.event;
18
19import com.android.calendar.CalendarEventModel;
20import com.android.calendar.CalendarEventModel.Attendee;
21import com.android.calendar.EmailAddressAdapter;
22import com.android.calendar.GeneralPreferences;
23import com.android.calendar.R;
24import com.android.calendar.TimezoneAdapter;
25import com.android.calendar.TimezoneAdapter.TimezoneRow;
26import com.android.calendar.Utils;
27import com.android.calendar.event.EditEventHelper.EditDoneRunnable;
28import com.android.common.Rfc822InputFilter;
29import com.android.common.Rfc822Validator;
30
31import android.app.Activity;
32import android.app.AlertDialog;
33import android.app.DatePickerDialog;
34import android.app.DatePickerDialog.OnDateSetListener;
35import android.app.ProgressDialog;
36import android.app.TimePickerDialog;
37import android.app.TimePickerDialog.OnTimeSetListener;
38import android.content.Context;
39import android.content.DialogInterface;
40import android.content.Intent;
41import android.content.SharedPreferences;
42import android.content.res.Resources;
43import android.database.Cursor;
44import android.pim.EventRecurrence;
45import android.provider.Calendar.Calendars;
46import android.provider.Settings;
47import android.text.InputFilter;
48import android.text.TextUtils;
49import android.text.format.DateFormat;
50import android.text.format.DateUtils;
51import android.text.format.Time;
52import android.text.util.Rfc822Tokenizer;
53import android.util.Log;
54import android.view.LayoutInflater;
55import android.view.View;
56import android.view.View.OnClickListener;
57import android.view.inputmethod.InputMethodManager;
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.ListView;
66import android.widget.MultiAutoCompleteTextView;
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    Spinner mResponseSpinner;
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    public void onCancel(DialogInterface dialog) {
600        if (dialog == mLoadingCalendarsDialog) {
601            mLoadingCalendarsDialog = null;
602            mSaveAfterQueryComplete = false;
603        } else if (dialog == mNoCalendarsDialog) {
604            mDone.setDoneCode(Utils.DONE_REVERT);
605            mDone.run();
606            return;
607        }
608    }
609
610    // This is called if the user clicks on a dialog button.
611    @Override
612    public void onClick(DialogInterface dialog, int which) {
613        if (dialog == mNoCalendarsDialog) {
614            mDone.setDoneCode(Utils.DONE_REVERT);
615            mDone.run();
616            if (which == DialogInterface.BUTTON_POSITIVE) {
617                Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT);
618                final String[] array = {"com.android.calendar"};
619                nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array);
620                mActivity.startActivity(nextIntent);
621            }
622        } else if (dialog == mTimezoneDialog) {
623            if (which >= 0 && which < mTimezoneAdapter.getCount()) {
624                setTimezone(which);
625                dialog.dismiss();
626            }
627        }
628    }
629
630    // Goes through the UI elements and updates the model as necessary
631    public boolean fillModelFromUI() {
632        if (mModel == null) {
633            return false;
634        }
635        mModel.mReminderMinutes = EventViewUtils.reminderItemsToMinutes(
636                mReminderItems, mReminderValues);
637        mModel.mHasAlarm = mReminderItems.size() > 0;
638        mModel.mTitle = mTitleTextView.getText().toString().trim();
639        mModel.mAllDay = mAllDayCheckBox.isChecked();
640        mModel.mLocation = mLocationTextView.getText().toString().trim();
641        mModel.mDescription = mDescriptionTextView.getText().toString().trim();
642        int position = mResponseSpinner.getSelectedItemPosition();
643        if (position > 0) {
644            mModel.mSelfAttendeeStatus = EditEventHelper.ATTENDEE_VALUES[position];
645        }
646
647        if (mAttendeesView != null && mAttendeesView.getChildCount() > 0) {
648            final int size = mAttendeesView.getChildCount();
649            mModel.mAttendeesList.clear();
650            for (int i = 0; i < size; i++) {
651                final Attendee attendee = mAttendeesView.getItem(i);
652                if (attendee == null || mAttendeesView.isMarkAsRemoved(i)) {
653                    continue;
654                }
655                mModel.addAttendee(attendee);
656            }
657        }
658
659        // If this was a new event we need to fill in the Calendar information
660        if (mModel.mUri == null) {
661            mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId();
662            int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
663            if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
664                String defaultCalendar = mCalendarsCursor.getString(
665                        EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
666                Utils.setSharedPreference(
667                        mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, defaultCalendar);
668                mModel.mOwnerAccount = defaultCalendar;
669                mModel.mOrganizer = defaultCalendar;
670                mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID);
671            }
672        }
673
674        if (mModel.mAllDay) {
675            // Reset start and end time, increment the monthDay by 1, and set
676            // the timezone to UTC, as required for all-day events.
677            mTimezone = Time.TIMEZONE_UTC;
678            mStartTime.hour = 0;
679            mStartTime.minute = 0;
680            mStartTime.second = 0;
681            mStartTime.timezone = mTimezone;
682            mModel.mStart = mStartTime.normalize(true);
683
684            // Round up to the next day
685            if (mEndTime.hour > 0 || mEndTime.minute > 0 || mEndTime.second > 0
686                    || mEndTime.monthDay == mStartTime.monthDay) {
687                mEndTime.monthDay++;
688            }
689            mEndTime.hour = 0;
690            mEndTime.minute = 0;
691            mEndTime.second = 0;
692            mEndTime.timezone = mTimezone;
693            mModel.mEnd = mEndTime.normalize(true);
694        } else {
695            mStartTime.timezone = mTimezone;
696            mEndTime.timezone = mTimezone;
697            mModel.mStart = mStartTime.toMillis(true);
698            mModel.mEnd = mEndTime.toMillis(true);
699        }
700        mModel.mTimezone = mTimezone;
701        mModel.mVisibility = mVisibilitySpinner.getSelectedItemPosition();
702        mModel.mTransparency = mTransparencySpinner.getSelectedItemPosition() != 0;
703
704        int selection;
705        // If we're making an exception we don't want it to be a repeating
706        // event.
707        if (mModification == EditEventHelper.MODIFY_SELECTED) {
708            selection = EditEventHelper.DOES_NOT_REPEAT;
709        } else {
710            position = mRepeatsSpinner.getSelectedItemPosition();
711            selection = mRecurrenceIndexes.get(position);
712        }
713
714        EditEventHelper.updateRecurrenceRule(
715                selection, mModel, Utils.getFirstDayOfWeek(mActivity) + 1);
716
717        // Save the timezone so we can display it as a standard option next time
718        if (!mModel.mAllDay) {
719            mTimezoneAdapter.saveRecentTimezone(mTimezone);
720        }
721        return true;
722    }
723
724    public EditEventView(Activity activity, View view, EditDoneRunnable done) {
725
726        mActivity = activity;
727        mView = view;
728        mDone = done;
729
730        // cache top level view elements
731        mLoadingMessage = (TextView) view.findViewById(R.id.loading_message);
732        mScrollView = (ScrollView) view.findViewById(R.id.scroll_view);
733
734        mLayoutInflater = activity.getLayoutInflater();
735
736        // cache all the widgets
737        mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars);
738        mViewList.add(mCalendarsSpinner);
739        mTitleTextView = (TextView) view.findViewById(R.id.title);
740        mViewList.add(mTitleTextView);
741        mLocationTextView = (TextView) view.findViewById(R.id.location);
742        mViewList.add(mLocationTextView);
743        mDescriptionTextView = (TextView) view.findViewById(R.id.description);
744        mViewList.add(mDescriptionTextView);
745        mTimezoneTextView = (TextView) view.findViewById(R.id.timezone_label);
746        mTimezoneFooterView = (TextView) mLayoutInflater.inflate(R.layout.timezone_footer, null);
747        mStartDateButton = (Button) view.findViewById(R.id.start_date);
748        mViewList.add(mStartDateButton);
749        mEndDateButton = (Button) view.findViewById(R.id.end_date);
750        mViewList.add(mEndDateButton);
751        mStartTimeButton = (Button) view.findViewById(R.id.start_time);
752        mViewList.add(mStartTimeButton);
753        mEndTimeButton = (Button) view.findViewById(R.id.end_time);
754        mViewList.add(mEndTimeButton);
755        mTimezoneButton = (Button) view.findViewById(R.id.timezone);
756        mViewList.add(mTimezoneButton);
757        mAllDayCheckBox = (CheckBox) view.findViewById(R.id.is_all_day);
758        mRepeatsSpinner = (Spinner) view.findViewById(R.id.repeats);
759        mViewList.add(mRepeatsSpinner);
760        mTransparencySpinner = (Spinner) view.findViewById(R.id.availability);
761        mViewList.add(mTransparencySpinner);
762        mVisibilitySpinner = (Spinner) view.findViewById(R.id.visibility);
763        mViewList.add(mVisibilitySpinner);
764
765        mResponseSpinner = (Spinner) view.findViewById(R.id.response_value);
766        mRemindersContainer = (LinearLayout) view.findViewById(R.id.reminder_items_container);
767
768        mSaveButton = (Button) view.findViewById(R.id.save);
769        mDeleteButton = (Button) view.findViewById(R.id.delete);
770
771        mDiscardButton = (Button) view.findViewById(R.id.discard);
772        mDiscardButton.setOnClickListener(this);
773
774        mAddAttendeesButton = (ImageButton) view.findViewById(R.id.add_attendee_button);
775        mAddAttendeesListener = new AddAttendeeClickListener();
776        mAddAttendeesButton.setOnClickListener(mAddAttendeesListener);
777
778        mStartTime = new Time();
779        mEndTime = new Time();
780        mTimezone = TimeZone.getDefault().getID();
781        mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
782
783        mAttendeesView = (AttendeesView)view.findViewById(R.id.attendee_list);
784
785        // Display loading screen
786        setModel(null);
787    }
788
789    /**
790     * Fill in the view with the contents of the given event model. This allows
791     * an edit view to be initialized before the event has been loaded. Passing
792     * in null for the model will display a loading screen. A non-null model
793     * will fill in the view's fields with the data contained in the model.
794     *
795     * @param model The event model to pull the data from
796     */
797    public void setModel(CalendarEventModel model) {
798        mModel = model;
799
800        // Need to close the autocomplete adapter to prevent leaking cursors.
801        if (mAddressAdapter != null) {
802            mAddressAdapter.close();
803            mAddressAdapter = null;
804        }
805
806        if (model == null) {
807            // Display loading screen
808            mLoadingMessage.setVisibility(View.VISIBLE);
809            mScrollView.setVisibility(View.GONE);
810            mSaveButton.setEnabled(false);
811            mDeleteButton.setEnabled(false);
812            return;
813        }
814
815        boolean canModifyCalendar = EditEventHelper.canModifyCalendar(model);
816        boolean canModifyEvent = EditEventHelper.canModifyEvent(model);
817        boolean canRespond = EditEventHelper.canRespond(model);
818
819        long begin = model.mStart;
820        long end = model.mEnd;
821        mTimezone = model.mTimezone; // this will be UTC for all day events
822
823        // Set up the starting times
824        if (begin > 0) {
825            mStartTime.timezone = mTimezone;
826            mStartTime.set(begin);
827            mStartTime.normalize(true);
828        }
829        if (end > 0) {
830            mEndTime.timezone = mTimezone;
831            mEndTime.set(end);
832            mEndTime.normalize(true);
833        }
834        String rrule = model.mRrule;
835        if (rrule != null) {
836            mEventRecurrence.parse(rrule);
837        }
838
839        // If the user is allowed to change the attendees set up the view and
840        // validator
841        if (!model.mHasAttendeeData) {
842            mView.findViewById(R.id.attendees_group).setVisibility(View.GONE);
843        } else if (!canModifyEvent) {
844            // Hide views used for adding attendees
845            mView.findViewById(R.id.add_attendees_label).setVisibility(View.GONE);
846            mView.findViewById(R.id.add_attendees_group).setVisibility(View.GONE);
847            mAddAttendeesButton.setVisibility(View.GONE);
848        } else {
849            String domain = "gmail.com";
850            if (!TextUtils.isEmpty(model.mOwnerAccount)) {
851                String ownerDomain = EditEventHelper.extractDomain(model.mOwnerAccount);
852                if (!TextUtils.isEmpty(ownerDomain)) {
853                    domain = ownerDomain;
854                }
855            }
856            mAddressAdapter = new EmailAddressAdapter(mActivity);
857            mEmailValidator = new Rfc822Validator(domain);
858            mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees);
859            mViewList.add(mAttendeesList);
860        }
861
862        if (canModifyEvent) {
863            mAllDayCheckBox
864                    .setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
865                        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
866                            if (isChecked) {
867                                if (mEndTime.hour == 0 && mEndTime.minute == 0) {
868                                    mEndTime.monthDay--;
869                                    long endMillis = mEndTime.normalize(true);
870
871                                    // Do not allow an event to have an end time
872                                    // before the
873                                    // start time.
874                                    if (mEndTime.before(mStartTime)) {
875                                        mEndTime.set(mStartTime);
876                                        endMillis = mEndTime.normalize(true);
877                                    }
878                                    setDate(mEndDateButton, endMillis);
879                                    setTime(mEndTimeButton, endMillis);
880                                }
881
882                                mStartTimeButton.setVisibility(View.GONE);
883                                mEndTimeButton.setVisibility(View.GONE);
884                                mTimezoneButton.setVisibility(View.GONE);
885                                mTimezoneTextView.setVisibility(View.GONE);
886                            } else {
887                                if (mEndTime.hour == 0 && mEndTime.minute == 0) {
888                                    mEndTime.monthDay++;
889                                    long endMillis = mEndTime.normalize(true);
890                                    setDate(mEndDateButton, endMillis);
891                                    setTime(mEndTimeButton, endMillis);
892                                }
893                                mStartTimeButton.setVisibility(View.VISIBLE);
894                                mEndTimeButton.setVisibility(View.VISIBLE);
895                                mTimezoneButton.setVisibility(View.VISIBLE);
896                                mTimezoneTextView.setVisibility(View.VISIBLE);
897                            }
898                        }
899                    });
900        } else {
901            // Hide all day if read only
902            mView.findViewById(R.id.is_all_day_label).setVisibility(View.GONE);
903            mAllDayCheckBox.setVisibility(View.GONE);
904        }
905
906        if (model.mAllDay) {
907            mAllDayCheckBox.setChecked(true);
908            // put things back in local time for all day events
909            mTimezone = TimeZone.getDefault().getID();
910            mStartTime.timezone = mTimezone;
911            mStartTime.normalize(true);
912            mEndTime.timezone = mTimezone;
913            mEndTime.normalize(true);
914        } else {
915            mAllDayCheckBox.setChecked(false);
916        }
917
918        mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
919        if (mTimezoneDialog != null) {
920            mTimezoneDialog.getListView().setAdapter(mTimezoneAdapter);
921        }
922
923        if (canRespond || canModifyEvent) {
924            mSaveButton.setOnClickListener(this);
925            mSaveButton.setEnabled(true);
926        } else {
927            mSaveButton.setEnabled(false);
928        }
929
930        if (canModifyCalendar) {
931            mDeleteButton.setOnClickListener(this);
932            mDeleteButton.setEnabled(true);
933        } else {
934            mDeleteButton.setEnabled(false);
935        }
936
937        // Initialize the reminder values array.
938        Resources r = mActivity.getResources();
939        String[] strings = r.getStringArray(R.array.reminder_minutes_values);
940        int size = strings.length;
941        ArrayList<Integer> list = new ArrayList<Integer>(size);
942        for (int i = 0; i < size; i++) {
943            list.add(Integer.parseInt(strings[i]));
944        }
945        mReminderValues = list;
946        String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
947        mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
948
949        SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity);
950        String durationString = prefs.getString(GeneralPreferences.KEY_DEFAULT_REMINDER, "0");
951        mDefaultReminderMinutes = Integer.parseInt(durationString);
952
953        int numReminders = 0;
954        if (model.mHasAlarm) {
955            ArrayList<Integer> minutes = model.mReminderMinutes;
956            numReminders = minutes.size();
957            for (Integer minute : minutes) {
958                EventViewUtils.addMinutesToList(
959                        mActivity, mReminderValues, mReminderLabels, minute);
960                EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
961                        mReminderValues, mReminderLabels, minute);
962            }
963        }
964
965        ImageButton reminderAddButton = (ImageButton) mView.findViewById(R.id.reminder_add);
966        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
967            public void onClick(View v) {
968                addReminder();
969            }
970        };
971        reminderAddButton.setOnClickListener(addReminderOnClickListener);
972        updateRemindersVisibility(numReminders);
973
974        mTitleTextView.setText(model.mTitle);
975        if (model.mIsOrganizer || TextUtils.isEmpty(model.mOrganizer)
976                || model.mOrganizer.endsWith(GOOGLE_SECONDARY_CALENDAR)) {
977            mView.findViewById(R.id.organizer_label).setVisibility(View.GONE);
978            mView.findViewById(R.id.organizer).setVisibility(View.GONE);
979        } else {
980            ((TextView) mView.findViewById(R.id.organizer)).setText(model.mOrganizerDisplayName);
981        }
982        mLocationTextView.setText(model.mLocation);
983        mDescriptionTextView.setText(model.mDescription);
984        mTransparencySpinner.setSelection(model.mTransparency ? 1 : 0);
985        mVisibilitySpinner.setSelection(model.mVisibility);
986        mResponseSpinner.setSelection(findResponseIndexFor(model.mSelfAttendeeStatus));
987        View responseLabel = mView.findViewById(R.id.response_label);
988        if (canRespond) {
989            responseLabel.setVisibility(View.VISIBLE);
990            mResponseSpinner.setVisibility(View.VISIBLE);
991        } else {
992            responseLabel.setVisibility(View.GONE);
993            mResponseSpinner.setVisibility(View.GONE);
994        }
995
996        if (model.mUri != null) {
997            // This is an existing event so hide the calendar spinner
998            // since we can't change the calendar.
999            View calendarGroup = mView.findViewById(R.id.calendar_group);
1000            calendarGroup.setVisibility(View.GONE);
1001        } else {
1002            mDeleteButton.setVisibility(View.GONE);
1003        }
1004
1005        populateWhen();
1006        populateTimezone();
1007        populateRepeats();
1008        updateAttendees(model.mAttendeesList);
1009        if (!canModifyEvent) {
1010            for (View v : mViewList) {
1011                v.setEnabled(false);
1012            }
1013        }
1014        mScrollView.setVisibility(View.VISIBLE);
1015        mLoadingMessage.setVisibility(View.GONE);
1016    }
1017
1018    private int findResponseIndexFor(int response) {
1019        int size = EditEventHelper.ATTENDEE_VALUES.length;
1020        for (int index = 0; index < size; index++) {
1021            if (EditEventHelper.ATTENDEE_VALUES[index] == response) {
1022                return index;
1023            }
1024        }
1025        return 0;
1026    }
1027
1028    public void setCalendarsCursor(Cursor cursor, boolean userVisible) {
1029        // If there are no syncable calendars, then we cannot allow
1030        // creating a new event.
1031        mCalendarsCursor = cursor;
1032        if (cursor == null || cursor.getCount() == 0) {
1033            // Cancel the "loading calendars" dialog if it exists
1034            if (mSaveAfterQueryComplete) {
1035                mLoadingCalendarsDialog.cancel();
1036            }
1037            if (!userVisible) {
1038                return;
1039            }
1040            // Create an error message for the user that, when clicked,
1041            // will exit this activity without saving the event.
1042            AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
1043            builder.setTitle(R.string.no_syncable_calendars)
1044                    .setIcon(android.R.drawable.ic_dialog_alert)
1045                    .setMessage(R.string.no_calendars_found)
1046                    .setPositiveButton(R.string.add_account, this)
1047                    .setNegativeButton(android.R.string.no, this)
1048                    .setOnCancelListener(this);
1049            mNoCalendarsDialog = builder.show();
1050            return;
1051        }
1052
1053        int defaultCalendarPosition = findDefaultCalendarPosition(cursor);
1054
1055        // populate the calendars spinner
1056        CalendarsAdapter adapter = new CalendarsAdapter(mActivity, cursor);
1057        mCalendarsSpinner.setAdapter(adapter);
1058        mCalendarsSpinner.setSelection(defaultCalendarPosition);
1059
1060        // Find user domain and set it to the validator.
1061        // TODO: we may want to update this validator if the user actually picks
1062        // a different calendar. maybe not. depends on what we want for the
1063        // user experience. this may change when we add support for multiple
1064        // accounts, anyway.
1065        if (mModel != null && mModel.mHasAttendeeData
1066                && cursor.moveToPosition(defaultCalendarPosition)) {
1067            String ownEmail = cursor.getString(EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
1068            if (ownEmail != null) {
1069                String domain = EditEventHelper.extractDomain(ownEmail);
1070                if (domain != null) {
1071                    mEmailValidator = new Rfc822Validator(domain);
1072                    mAttendeesList.setValidator(mEmailValidator);
1073                }
1074            }
1075        }
1076        if (mSaveAfterQueryComplete) {
1077            mLoadingCalendarsDialog.cancel();
1078            if (prepareForSave() && fillModelFromUI()) {
1079                int exit = userVisible ? Utils.DONE_EXIT : 0;
1080                mDone.setDoneCode(Utils.DONE_SAVE | exit);
1081                mDone.run();
1082            } else if (userVisible) {
1083                mDone.setDoneCode(Utils.DONE_EXIT);
1084                mDone.run();
1085            } else if (Log.isLoggable(TAG, Log.DEBUG)) {
1086                Log.d(TAG, "SetCalendarsCursor:Save failed and unable to exit view");
1087            }
1088            return;
1089        }
1090    }
1091
1092    public void setModification(int modifyWhich) {
1093        mModification = modifyWhich;
1094        // If we are modifying all the events in a
1095        // series then disable and ignore the date.
1096        if (modifyWhich == Utils.MODIFY_ALL) {
1097            mStartDateButton.setEnabled(false);
1098            mEndDateButton.setEnabled(false);
1099        } else if (modifyWhich == Utils.MODIFY_SELECTED) {
1100            mRepeatsSpinner.setEnabled(false);
1101        }
1102    }
1103
1104    // Find the calendar position in the cursor that matches calendar in
1105    // preference
1106    private int findDefaultCalendarPosition(Cursor calendarsCursor) {
1107        if (calendarsCursor.getCount() <= 0) {
1108            return -1;
1109        }
1110
1111        String defaultCalendar = Utils.getSharedPreference(
1112                mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, null);
1113
1114        if (defaultCalendar == null) {
1115            return 0;
1116        }
1117        int calendarsOwnerColumn = calendarsCursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
1118        int position = 0;
1119        calendarsCursor.moveToPosition(-1);
1120        while (calendarsCursor.moveToNext()) {
1121            if (defaultCalendar.equals(calendarsCursor.getString(calendarsOwnerColumn))) {
1122                return position;
1123            }
1124            position++;
1125        }
1126        return 0;
1127    }
1128
1129    public void updateAttendees(HashMap<String, Attendee> attendeesList) {
1130        mAttendeesView.setRfc822Validator(mEmailValidator);
1131        mAttendeesView.addAttendees(attendeesList);
1132    }
1133
1134    private void updateRemindersVisibility(int numReminders) {
1135        if (numReminders == 0) {
1136            mRemindersContainer.setVisibility(View.GONE);
1137        } else {
1138            mRemindersContainer.setVisibility(View.VISIBLE);
1139        }
1140    }
1141
1142    public void addReminder() {
1143        // TODO: when adding a new reminder, make it different from the
1144        // last one in the list (if any).
1145        if (mDefaultReminderMinutes == 0) {
1146            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
1147                    mReminderValues, mReminderLabels, 10 /* minutes */);
1148        } else {
1149            EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderItems,
1150                    mReminderValues, mReminderLabels, mDefaultReminderMinutes);
1151        }
1152        updateRemindersVisibility(mReminderItems.size());
1153        mScrollView.fling(REMINDER_FLING_VELOCITY);
1154    }
1155
1156    public int getReminderCount() {
1157        return mReminderItems.size();
1158    }
1159
1160    // From com.google.android.gm.ComposeActivity
1161    private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) {
1162        MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) mView.findViewById(res);
1163        list.setAdapter(mAddressAdapter);
1164        list.setTokenizer(new Rfc822Tokenizer());
1165        list.setValidator(mEmailValidator);
1166
1167        // NOTE: assumes no other filters are set
1168        list.setFilters(sRecipientFilters);
1169
1170        return list;
1171    }
1172
1173    /**
1174     * From com.google.android.gm.ComposeActivity Implements special address
1175     * cleanup rules: The first space key entry following an "@" symbol that is
1176     * followed by any combination of letters and symbols, including one+ dots
1177     * and zero commas, should insert an extra comma (followed by the space).
1178     */
1179    private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() };
1180
1181    private void setDate(TextView view, long millis) {
1182        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
1183                | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH
1184                | DateUtils.FORMAT_ABBREV_WEEKDAY;
1185
1186        // Unfortunately, DateUtils doesn't support a timezone other than the
1187        // default timezone provided by the system, so we have this ugly hack
1188        // here to trick it into formatting our time correctly. In order to
1189        // prevent all sorts of craziness, we synchronize on the TimeZone class
1190        // to prevent other threads from reading an incorrect timezone from
1191        // calls to TimeZone#getDefault()
1192        // TODO fix this if/when DateUtils allows for passing in a timezone
1193        String dateString;
1194        synchronized (TimeZone.class) {
1195            TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
1196            dateString = DateUtils.formatDateTime(mActivity, millis, flags);
1197            // setting the default back to null restores the correct behavior
1198            TimeZone.setDefault(null);
1199        }
1200        view.setText(dateString);
1201    }
1202
1203    private void setTime(TextView view, long millis) {
1204        int flags = DateUtils.FORMAT_SHOW_TIME;
1205        if (DateFormat.is24HourFormat(mActivity)) {
1206            flags |= DateUtils.FORMAT_24HOUR;
1207        }
1208
1209        // Unfortunately, DateUtils doesn't support a timezone other than the
1210        // default timezone provided by the system, so we have this ugly hack
1211        // here to trick it into formatting our time correctly. In order to
1212        // prevent all sorts of craziness, we synchronize on the TimeZone class
1213        // to prevent other threads from reading an incorrect timezone from
1214        // calls to TimeZone#getDefault()
1215        // TODO fix this if/when DateUtils allows for passing in a timezone
1216        String timeString;
1217        synchronized (TimeZone.class) {
1218            TimeZone.setDefault(TimeZone.getTimeZone(mTimezone));
1219            timeString = DateUtils.formatDateTime(mActivity, millis, flags);
1220            TimeZone.setDefault(null);
1221        }
1222        view.setText(timeString);
1223    }
1224
1225    private void setTimezone(int i) {
1226        if (i < 0 || i >= mTimezoneAdapter.getCount()) {
1227            return; // do nothing
1228        }
1229        TimezoneRow timezone = mTimezoneAdapter.getItem(i);
1230        mTimezoneButton.setText(timezone.toString());
1231        mTimezone = timezone.mId;
1232        mStartTime.timezone = mTimezone;
1233        mStartTime.normalize(true);
1234        mEndTime.timezone = mTimezone;
1235        mEndTime.normalize(true);
1236        mTimezoneAdapter.setCurrentTimezone(mTimezone);
1237    }
1238}
1239