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