OSDN Git Service

am c8c1fc5c: (-s ours) Import revised translations. DO NOT MERGE
[android-x86/packages-apps-Calendar.git] / src / com / android / calendar / EditEvent.java
1 /*
2  * Copyright (C) 2008 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
17 package com.android.calendar;
18
19 import static android.provider.Calendar.EVENT_BEGIN_TIME;
20 import static android.provider.Calendar.EVENT_END_TIME;
21
22 import android.app.Activity;
23 import android.app.AlertDialog;
24 import android.app.DatePickerDialog;
25 import android.app.ProgressDialog;
26 import android.app.TimePickerDialog;
27 import android.app.DatePickerDialog.OnDateSetListener;
28 import android.app.TimePickerDialog.OnTimeSetListener;
29 import android.content.AsyncQueryHandler;
30 import android.content.ContentProviderOperation;
31 import android.content.ContentProviderResult;
32 import android.content.ContentResolver;
33 import android.content.ContentUris;
34 import android.content.ContentValues;
35 import android.content.Context;
36 import android.content.DialogInterface;
37 import android.content.Intent;
38 import android.content.OperationApplicationException;
39 import android.content.SharedPreferences;
40 import android.content.ContentProviderOperation.Builder;
41 import android.content.DialogInterface.OnCancelListener;
42 import android.content.DialogInterface.OnClickListener;
43 import android.content.res.Resources;
44 import android.database.Cursor;
45 import android.net.Uri;
46 import android.os.Bundle;
47 import android.os.RemoteException;
48 import android.pim.EventRecurrence;
49 import android.preference.PreferenceManager;
50 import android.provider.Calendar.Attendees;
51 import android.provider.Calendar.Calendars;
52 import android.provider.Calendar.Events;
53 import android.provider.Calendar.Reminders;
54 import android.text.Editable;
55 import android.text.InputFilter;
56 import android.text.SpannableStringBuilder;
57 import android.text.Spanned;
58 import android.text.TextUtils;
59 import android.text.format.DateFormat;
60 import android.text.format.DateUtils;
61 import android.text.format.Time;
62 import android.text.util.Rfc822Token;
63 import android.text.util.Rfc822Tokenizer;
64 import android.text.util.Rfc822Validator;
65 import android.util.Log;
66 import android.view.KeyEvent;
67 import android.view.LayoutInflater;
68 import android.view.Menu;
69 import android.view.MenuItem;
70 import android.view.View;
71 import android.view.Window;
72 import android.widget.ArrayAdapter;
73 import android.widget.Button;
74 import android.widget.CheckBox;
75 import android.widget.CompoundButton;
76 import android.widget.DatePicker;
77 import android.widget.ImageButton;
78 import android.widget.LinearLayout;
79 import android.widget.MultiAutoCompleteTextView;
80 import android.widget.ResourceCursorAdapter;
81 import android.widget.Spinner;
82 import android.widget.TextView;
83 import android.widget.TimePicker;
84 import android.widget.Toast;
85
86 import java.util.ArrayList;
87 import java.util.Arrays;
88 import java.util.Calendar;
89 import java.util.TimeZone;
90
91 public class EditEvent extends Activity implements View.OnClickListener,
92         DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
93     private static final String TAG = "EditEvent";
94     private static final boolean DEBUG = false;
95
96     /**
97      * This is the symbolic name for the key used to pass in the boolean
98      * for creating all-day events that is part of the extra data of the intent.
99      * This is used only for creating new events and is set to true if
100      * the default for the new event should be an all-day event.
101      */
102     public static final String EVENT_ALL_DAY = "allDay";
103
104     private static final int MAX_REMINDERS = 5;
105
106     private static final int MENU_GROUP_REMINDER = 1;
107     private static final int MENU_GROUP_SHOW_OPTIONS = 2;
108     private static final int MENU_GROUP_HIDE_OPTIONS = 3;
109
110     private static final int MENU_ADD_REMINDER = 1;
111     private static final int MENU_SHOW_EXTRA_OPTIONS = 2;
112     private static final int MENU_HIDE_EXTRA_OPTIONS = 3;
113
114     private static final String[] EVENT_PROJECTION = new String[] {
115             Events._ID,             // 0
116             Events.TITLE,           // 1
117             Events.DESCRIPTION,     // 2
118             Events.EVENT_LOCATION,  // 3
119             Events.ALL_DAY,         // 4
120             Events.HAS_ALARM,       // 5
121             Events.CALENDAR_ID,     // 6
122             Events.DTSTART,         // 7
123             Events.DURATION,        // 8
124             Events.EVENT_TIMEZONE,  // 9
125             Events.RRULE,           // 10
126             Events._SYNC_ID,        // 11
127             Events.TRANSPARENCY,    // 12
128             Events.VISIBILITY,      // 13
129     };
130     private static final int EVENT_INDEX_ID = 0;
131     private static final int EVENT_INDEX_TITLE = 1;
132     private static final int EVENT_INDEX_DESCRIPTION = 2;
133     private static final int EVENT_INDEX_EVENT_LOCATION = 3;
134     private static final int EVENT_INDEX_ALL_DAY = 4;
135     private static final int EVENT_INDEX_HAS_ALARM = 5;
136     private static final int EVENT_INDEX_CALENDAR_ID = 6;
137     private static final int EVENT_INDEX_DTSTART = 7;
138     private static final int EVENT_INDEX_DURATION = 8;
139     private static final int EVENT_INDEX_TIMEZONE = 9;
140     private static final int EVENT_INDEX_RRULE = 10;
141     private static final int EVENT_INDEX_SYNC_ID = 11;
142     private static final int EVENT_INDEX_TRANSPARENCY = 12;
143     private static final int EVENT_INDEX_VISIBILITY = 13;
144
145     private static final String[] CALENDARS_PROJECTION = new String[] {
146             Calendars._ID,           // 0
147             Calendars.DISPLAY_NAME,  // 1
148             Calendars.OWNER_ACCOUNT, // 2
149     };
150     private static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
151     private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
152     private static final String CALENDARS_WHERE = Calendars.ACCESS_LEVEL + ">=" +
153             Calendars.CONTRIBUTOR_ACCESS + " AND " + Calendars.SYNC_EVENTS + "=1";
154
155     private static final String[] REMINDERS_PROJECTION = new String[] {
156             Reminders._ID,      // 0
157             Reminders.MINUTES,  // 1
158     };
159     private static final int REMINDERS_INDEX_MINUTES = 1;
160     private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
161             Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
162             Reminders.METHOD_DEFAULT + ")";
163
164     private static final String[] ATTENDEES_PROJECTION = new String[] {
165         Attendees.ATTENDEE_NAME,            // 0
166         Attendees.ATTENDEE_EMAIL,           // 1
167     };
168     private static final int ATTENDEES_INDEX_NAME = 0;
169     private static final int ATTENDEES_INDEX_EMAIL = 1;
170     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=? AND "
171             + Attendees.ATTENDEE_RELATIONSHIP + "<>" + Attendees.RELATIONSHIP_ORGANIZER;
172
173     private static final int DOES_NOT_REPEAT = 0;
174     private static final int REPEATS_DAILY = 1;
175     private static final int REPEATS_EVERY_WEEKDAY = 2;
176     private static final int REPEATS_WEEKLY_ON_DAY = 3;
177     private static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
178     private static final int REPEATS_MONTHLY_ON_DAY = 5;
179     private static final int REPEATS_YEARLY = 6;
180     private static final int REPEATS_CUSTOM = 7;
181
182     private static final int MODIFY_UNINITIALIZED = 0;
183     private static final int MODIFY_SELECTED = 1;
184     private static final int MODIFY_ALL = 2;
185     private static final int MODIFY_ALL_FOLLOWING = 3;
186
187     private static final int DAY_IN_SECONDS = 24 * 60 * 60;
188
189     private int mFirstDayOfWeek; // cached in onCreate
190     private Uri mUri;
191     private Cursor mEventCursor;
192     private Cursor mCalendarsCursor;
193
194     private Button mStartDateButton;
195     private Button mEndDateButton;
196     private Button mStartTimeButton;
197     private Button mEndTimeButton;
198     private Button mSaveButton;
199     private Button mDeleteButton;
200     private Button mDiscardButton;
201     private CheckBox mAllDayCheckBox;
202     private Spinner mCalendarsSpinner;
203     private Spinner mRepeatsSpinner;
204     private Spinner mAvailabilitySpinner;
205     private Spinner mVisibilitySpinner;
206     private TextView mTitleTextView;
207     private TextView mLocationTextView;
208     private TextView mDescriptionTextView;
209     private View mRemindersSeparator;
210     private LinearLayout mRemindersContainer;
211     private LinearLayout mExtraOptions;
212     private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
213     private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
214     MultiAutoCompleteTextView mAttendeesList;
215     private EmailAddressAdapter mAddressAdapter;
216     private String mOriginalAttendees = "";
217
218     private EventRecurrence mEventRecurrence = new EventRecurrence();
219     private String mRrule;
220     private boolean mCalendarsQueryComplete;
221     private boolean mSaveAfterQueryComplete;
222     private ProgressDialog mLoadingCalendarsDialog;
223     private AlertDialog mNoCalendarsDialog;
224     private ContentValues mInitialValues;
225
226     /**
227      * If the repeating event is created on the phone and it hasn't been
228      * synced yet to the web server, then there is a bug where you can't
229      * delete or change an instance of the repeating event.  This case
230      * can be detected with mSyncId.  If mSyncId == null, then the repeating
231      * event has not been synced to the phone, in which case we won't allow
232      * the user to change one instance.
233      */
234     private String mSyncId;
235
236     private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0);
237     private ArrayList<Integer> mReminderValues;
238     private ArrayList<String> mReminderLabels;
239
240     private Time mStartTime;
241     private Time mEndTime;
242     private int mModification = MODIFY_UNINITIALIZED;
243     private int mDefaultReminderMinutes;
244
245     private DeleteEventHelper mDeleteEventHelper;
246     private QueryHandler mQueryHandler;
247
248     /* This class is used to update the time buttons. */
249     private class TimeListener implements OnTimeSetListener {
250         private View mView;
251
252         public TimeListener(View view) {
253             mView = view;
254         }
255
256         public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
257             // Cache the member variables locally to avoid inner class overhead.
258             Time startTime = mStartTime;
259             Time endTime = mEndTime;
260
261             // Cache the start and end millis so that we limit the number
262             // of calls to normalize() and toMillis(), which are fairly
263             // expensive.
264             long startMillis;
265             long endMillis;
266             if (mView == mStartTimeButton) {
267                 // The start time was changed.
268                 int hourDuration = endTime.hour - startTime.hour;
269                 int minuteDuration = endTime.minute - startTime.minute;
270
271                 startTime.hour = hourOfDay;
272                 startTime.minute = minute;
273                 startMillis = startTime.normalize(true);
274
275                 // Also update the end time to keep the duration constant.
276                 endTime.hour = hourOfDay + hourDuration;
277                 endTime.minute = minute + minuteDuration;
278                 endMillis = endTime.normalize(true);
279             } else {
280                 // The end time was changed.
281                 startMillis = startTime.toMillis(true);
282                 endTime.hour = hourOfDay;
283                 endTime.minute = minute;
284                 endMillis = endTime.normalize(true);
285
286                 // Do not allow an event to have an end time before the start time.
287                 if (endTime.before(startTime)) {
288                     endTime.set(startTime);
289                     endMillis = startMillis;
290                 }
291             }
292
293             setDate(mEndDateButton, endMillis);
294             setTime(mStartTimeButton, startMillis);
295             setTime(mEndTimeButton, endMillis);
296         }
297     }
298
299     private class TimeClickListener implements View.OnClickListener {
300         private Time mTime;
301
302         public TimeClickListener(Time time) {
303             mTime = time;
304         }
305
306         public void onClick(View v) {
307             new TimePickerDialog(EditEvent.this, new TimeListener(v),
308                     mTime.hour, mTime.minute,
309                     DateFormat.is24HourFormat(EditEvent.this)).show();
310         }
311     }
312
313     private class DateListener implements OnDateSetListener {
314         View mView;
315
316         public DateListener(View view) {
317             mView = view;
318         }
319
320         public void onDateSet(DatePicker view, int year, int month, int monthDay) {
321             // Cache the member variables locally to avoid inner class overhead.
322             Time startTime = mStartTime;
323             Time endTime = mEndTime;
324
325             // Cache the start and end millis so that we limit the number
326             // of calls to normalize() and toMillis(), which are fairly
327             // expensive.
328             long startMillis;
329             long endMillis;
330             if (mView == mStartDateButton) {
331                 // The start date was changed.
332                 int yearDuration = endTime.year - startTime.year;
333                 int monthDuration = endTime.month - startTime.month;
334                 int monthDayDuration = endTime.monthDay - startTime.monthDay;
335
336                 startTime.year = year;
337                 startTime.month = month;
338                 startTime.monthDay = monthDay;
339                 startMillis = startTime.normalize(true);
340
341                 // Also update the end date to keep the duration constant.
342                 endTime.year = year + yearDuration;
343                 endTime.month = month + monthDuration;
344                 endTime.monthDay = monthDay + monthDayDuration;
345                 endMillis = endTime.normalize(true);
346
347                 // If the start date has changed then update the repeats.
348                 populateRepeats();
349             } else {
350                 // The end date was changed.
351                 startMillis = startTime.toMillis(true);
352                 endTime.year = year;
353                 endTime.month = month;
354                 endTime.monthDay = monthDay;
355                 endMillis = endTime.normalize(true);
356
357                 // Do not allow an event to have an end time before the start time.
358                 if (endTime.before(startTime)) {
359                     endTime.set(startTime);
360                     endMillis = startMillis;
361                 }
362             }
363
364             setDate(mStartDateButton, startMillis);
365             setDate(mEndDateButton, endMillis);
366             setTime(mEndTimeButton, endMillis); // In case end time had to be reset
367         }
368     }
369
370     private class DateClickListener implements View.OnClickListener {
371         private Time mTime;
372
373         public DateClickListener(Time time) {
374             mTime = time;
375         }
376
377         public void onClick(View v) {
378             new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year,
379                     mTime.month, mTime.monthDay).show();
380         }
381     }
382
383     static private class CalendarsAdapter extends ResourceCursorAdapter {
384         public CalendarsAdapter(Context context, Cursor c) {
385             super(context, R.layout.calendars_item, c);
386             setDropDownViewResource(R.layout.calendars_dropdown_item);
387         }
388
389         @Override
390         public void bindView(View view, Context context, Cursor cursor) {
391             TextView name = (TextView) view.findViewById(R.id.calendar_name);
392             name.setText(cursor.getString(CALENDARS_INDEX_DISPLAY_NAME));
393         }
394     }
395
396     // This is called if the user clicks on one of the buttons: "Save",
397     // "Discard", or "Delete".  This is also called if the user clicks
398     // on the "remove reminder" button.
399     public void onClick(View v) {
400         if (v == mSaveButton) {
401             if (save()) {
402                 finish();
403             }
404             return;
405         }
406
407         if (v == mDeleteButton) {
408             long begin = mStartTime.toMillis(false /* use isDst */);
409             long end = mEndTime.toMillis(false /* use isDst */);
410             int which = -1;
411             switch (mModification) {
412             case MODIFY_SELECTED:
413                 which = DeleteEventHelper.DELETE_SELECTED;
414                 break;
415             case MODIFY_ALL_FOLLOWING:
416                 which = DeleteEventHelper.DELETE_ALL_FOLLOWING;
417                 break;
418             case MODIFY_ALL:
419                 which = DeleteEventHelper.DELETE_ALL;
420                 break;
421             }
422             mDeleteEventHelper.delete(begin, end, mEventCursor, which);
423             return;
424         }
425
426         if (v == mDiscardButton) {
427             finish();
428             return;
429         }
430
431         // This must be a click on one of the "remove reminder" buttons
432         LinearLayout reminderItem = (LinearLayout) v.getParent();
433         LinearLayout parent = (LinearLayout) reminderItem.getParent();
434         parent.removeView(reminderItem);
435         mReminderItems.remove(reminderItem);
436         updateRemindersVisibility();
437     }
438
439     // This is called if the user cancels a popup dialog.  There are two
440     // dialogs: the "Loading calendars" dialog, and the "No calendars"
441     // dialog.  The "Loading calendars" dialog is shown if there is a delay
442     // in loading the calendars (needed when creating an event) and the user
443     // tries to save the event before the calendars have finished loading.
444     // The "No calendars" dialog is shown if there are no syncable calendars.
445     public void onCancel(DialogInterface dialog) {
446         if (dialog == mLoadingCalendarsDialog) {
447             mSaveAfterQueryComplete = false;
448         } else if (dialog == mNoCalendarsDialog) {
449             finish();
450         }
451     }
452
453     // This is called if the user clicks on a dialog button.
454     public void onClick(DialogInterface dialog, int which) {
455         if (dialog == mNoCalendarsDialog) {
456             finish();
457         }
458     }
459
460     private class QueryHandler extends AsyncQueryHandler {
461         public QueryHandler(ContentResolver cr) {
462             super(cr);
463         }
464
465         @Override
466         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
467             // If the Activity is finishing, then close the cursor.
468             // Otherwise, use the new cursor in the adapter.
469             if (isFinishing()) {
470                 stopManagingCursor(cursor);
471                 cursor.close();
472             } else {
473                 mCalendarsCursor = cursor;
474                 startManagingCursor(cursor);
475
476                 // Stop the spinner
477                 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
478                         Window.PROGRESS_VISIBILITY_OFF);
479
480                 // If there are no syncable calendars, then we cannot allow
481                 // creating a new event.
482                 if (cursor.getCount() == 0) {
483                     // Cancel the "loading calendars" dialog if it exists
484                     if (mSaveAfterQueryComplete) {
485                         mLoadingCalendarsDialog.cancel();
486                     }
487
488                     // Create an error message for the user that, when clicked,
489                     // will exit this activity without saving the event.
490                     AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
491                     builder.setTitle(R.string.no_syncable_calendars)
492                         .setIcon(android.R.drawable.ic_dialog_alert)
493                         .setMessage(R.string.no_calendars_found)
494                         .setPositiveButton(android.R.string.ok, EditEvent.this)
495                         .setOnCancelListener(EditEvent.this);
496                     mNoCalendarsDialog = builder.show();
497                     return;
498                 }
499
500                 // Find user domain and set it to the validator
501                 // TODO This won't work realiably if there are calendars in different domains
502                 cursor.moveToFirst();
503                 String ownEmail = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
504                 if (ownEmail != null) {
505                     int separator = ownEmail.lastIndexOf('@');
506                     if (separator != -1 && ++separator < ownEmail.length()) {
507                         mAttendeesList.setValidator(new Rfc822Validator(ownEmail
508                                 .substring(separator)));
509                     }
510                 }
511
512                 // populate the calendars spinner
513                 CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor);
514                 mCalendarsSpinner.setAdapter(adapter);
515                 mCalendarsQueryComplete = true;
516                 if (mSaveAfterQueryComplete) {
517                     mLoadingCalendarsDialog.cancel();
518                     save();
519                     finish();
520                 }
521             }
522         }
523     }
524
525     @Override
526     protected void onCreate(Bundle icicle) {
527         super.onCreate(icicle);
528         requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
529         setContentView(R.layout.edit_event);
530
531         mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
532
533         mStartTime = new Time();
534         mEndTime = new Time();
535
536         Intent intent = getIntent();
537         mUri = intent.getData();
538
539         if (mUri != null) {
540             mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null);
541             if (mEventCursor == null || mEventCursor.getCount() == 0) {
542                 // The cursor is empty. This can happen if the event was deleted.
543                 finish();
544                 return;
545             }
546         }
547
548         long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
549         long end = intent.getLongExtra(EVENT_END_TIME, 0);
550
551         boolean allDay = false;
552         if (mEventCursor != null) {
553             // The event already exists so fetch the all-day status
554             mEventCursor.moveToFirst();
555             allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
556             String rrule = mEventCursor.getString(EVENT_INDEX_RRULE);
557             String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
558             long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
559
560             // Remember the initial values
561             mInitialValues = new ContentValues();
562             mInitialValues.put(EVENT_BEGIN_TIME, begin);
563             mInitialValues.put(EVENT_END_TIME, end);
564             mInitialValues.put(Events.ALL_DAY, allDay ? 1 : 0);
565             mInitialValues.put(Events.RRULE, rrule);
566             mInitialValues.put(Events.EVENT_TIMEZONE, timezone);
567             mInitialValues.put(Events.CALENDAR_ID, calendarId);
568         } else {
569             // We are creating a new event, so set the default from the
570             // intent (if specified).
571             allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false);
572
573             // Start the spinner
574             getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
575                     Window.PROGRESS_VISIBILITY_ON);
576
577             // Start a query in the background to read the list of calendars
578             mQueryHandler = new QueryHandler(getContentResolver());
579             mQueryHandler.startQuery(0, null, Calendars.CONTENT_URI, CALENDARS_PROJECTION,
580                     CALENDARS_WHERE, null /* selection args */, null /* sort order */);
581         }
582
583         // If the event is all-day, read the times in UTC timezone
584         if (begin != 0) {
585             if (allDay) {
586                 String tz = mStartTime.timezone;
587                 mStartTime.timezone = Time.TIMEZONE_UTC;
588                 mStartTime.set(begin);
589                 mStartTime.timezone = tz;
590
591                 // Calling normalize to calculate isDst
592                 mStartTime.normalize(true);
593             } else {
594                 mStartTime.set(begin);
595             }
596         }
597
598         if (end != 0) {
599             if (allDay) {
600                 String tz = mStartTime.timezone;
601                 mEndTime.timezone = Time.TIMEZONE_UTC;
602                 mEndTime.set(end);
603                 mEndTime.timezone = tz;
604
605                 // Calling normalize to calculate isDst
606                 mEndTime.normalize(true);
607             } else {
608                 mEndTime.set(end);
609             }
610         }
611
612         // cache all the widgets
613         mTitleTextView = (TextView) findViewById(R.id.title);
614         mLocationTextView = (TextView) findViewById(R.id.location);
615         mDescriptionTextView = (TextView) findViewById(R.id.description);
616         mStartDateButton = (Button) findViewById(R.id.start_date);
617         mEndDateButton = (Button) findViewById(R.id.end_date);
618         mStartTimeButton = (Button) findViewById(R.id.start_time);
619         mEndTimeButton = (Button) findViewById(R.id.end_time);
620         mAllDayCheckBox = (CheckBox) findViewById(R.id.is_all_day);
621         mCalendarsSpinner = (Spinner) findViewById(R.id.calendars);
622         mRepeatsSpinner = (Spinner) findViewById(R.id.repeats);
623         mAvailabilitySpinner = (Spinner) findViewById(R.id.availability);
624         mVisibilitySpinner = (Spinner) findViewById(R.id.visibility);
625         mRemindersSeparator = findViewById(R.id.reminders_separator);
626         mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
627         mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
628
629         mAddressAdapter = new EmailAddressAdapter(this);
630         mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees, R.string.hint_attendees);
631
632         mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
633             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
634                 if (isChecked) {
635                     if (mEndTime.hour == 0 && mEndTime.minute == 0) {
636                         mEndTime.monthDay--;
637                         long endMillis = mEndTime.normalize(true);
638
639                         // Do not allow an event to have an end time before the start time.
640                         if (mEndTime.before(mStartTime)) {
641                             mEndTime.set(mStartTime);
642                             endMillis = mEndTime.normalize(true);
643                         }
644                         setDate(mEndDateButton, endMillis);
645                         setTime(mEndTimeButton, endMillis);
646                     }
647
648                     mStartTimeButton.setVisibility(View.GONE);
649                     mEndTimeButton.setVisibility(View.GONE);
650                 } else {
651                     if (mEndTime.hour == 0 && mEndTime.minute == 0) {
652                         mEndTime.monthDay++;
653                         long endMillis = mEndTime.normalize(true);
654                         setDate(mEndDateButton, endMillis);
655                         setTime(mEndTimeButton, endMillis);
656                     }
657
658                     mStartTimeButton.setVisibility(View.VISIBLE);
659                     mEndTimeButton.setVisibility(View.VISIBLE);
660                 }
661             }
662         });
663
664         if (allDay) {
665             mAllDayCheckBox.setChecked(true);
666         } else {
667             mAllDayCheckBox.setChecked(false);
668         }
669
670         mSaveButton = (Button) findViewById(R.id.save);
671         mSaveButton.setOnClickListener(this);
672
673         mDeleteButton = (Button) findViewById(R.id.delete);
674         mDeleteButton.setOnClickListener(this);
675
676         mDiscardButton = (Button) findViewById(R.id.discard);
677         mDiscardButton.setOnClickListener(this);
678
679         // Initialize the reminder values array.
680         Resources r = getResources();
681         String[] strings = r.getStringArray(R.array.reminder_minutes_values);
682         int size = strings.length;
683         ArrayList<Integer> list = new ArrayList<Integer>(size);
684         for (int i = 0 ; i < size ; i++) {
685             list.add(Integer.parseInt(strings[i]));
686         }
687         mReminderValues = list;
688         String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
689         mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
690
691         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
692         String durationString =
693                 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
694         mDefaultReminderMinutes = Integer.parseInt(durationString);
695
696         long eventId = (mEventCursor == null) ? -1 : mEventCursor.getLong(EVENT_INDEX_ID);
697         ContentResolver cr = getContentResolver();
698
699         // Reminders cursor
700         boolean hasAlarm = (mEventCursor != null)
701                 && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0);
702         if (hasAlarm) {
703             Uri uri = Reminders.CONTENT_URI;
704             String where = String.format(REMINDERS_WHERE, eventId);
705             Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
706             try {
707                 // First pass: collect all the custom reminder minutes (e.g.,
708                 // a reminder of 8 minutes) into a global list.
709                 while (reminderCursor.moveToNext()) {
710                     int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
711                     EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
712                 }
713
714                 // Second pass: create the reminder spinners
715                 reminderCursor.moveToPosition(-1);
716                 while (reminderCursor.moveToNext()) {
717                     int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
718                     mOriginalMinutes.add(minutes);
719                     EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
720                             mReminderLabels, minutes);
721                 }
722             } finally {
723                 reminderCursor.close();
724             }
725         }
726         updateRemindersVisibility();
727
728         // Setup the + Add Reminder Button
729         View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
730             public void onClick(View v) {
731                 addReminder();
732             }
733         };
734         ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
735         reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
736
737         mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
738
739        // Attendees cursor
740         if (eventId != -1) {
741             Uri uri = Attendees.CONTENT_URI;
742             String[] whereArgs = {Long.toString(eventId)};
743             Cursor attendeeCursor = cr.query(uri, ATTENDEES_PROJECTION, ATTENDEES_WHERE, whereArgs,
744                     null);
745             try {
746                 StringBuilder b = new StringBuilder();
747                 while (attendeeCursor.moveToNext()) {
748                     String name = attendeeCursor.getString(ATTENDEES_INDEX_NAME);
749                     String email = attendeeCursor.getString(ATTENDEES_INDEX_EMAIL);
750                     if (email != null) {
751                         if (name != null && name.length() > 0 && !name.equals(email)) {
752                             b.append('"').append(name).append("\" ");
753                         }
754                         b.append('<').append(email).append(">, ");
755                     }
756                 }
757                 if (b.length() > 0) {
758                     mOriginalAttendees = b.toString();
759                     mAttendeesList.setText(mOriginalAttendees);
760                 }
761             } finally {
762                 attendeeCursor.close();
763             }
764         }
765         if (mEventCursor == null) {
766             // Allow the intent to specify the fields in the event.
767             // This will allow other apps to create events easily.
768             initFromIntent(intent);
769         }
770     }
771
772     private Rfc822Token[] getAddressesFromList(MultiAutoCompleteTextView list) {
773         list.clearComposingText();
774         return Rfc822Tokenizer.tokenize(list.getText());
775     }
776
777     // From com.google.android.gm.ComposeActivity
778     private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res, int hintId) {
779         MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res);
780         list.setAdapter(mAddressAdapter);
781         list.setTokenizer(new Rfc822Tokenizer());
782
783         // NOTE: assumes no other filters are set
784         list.setFilters(sRecipientFilters);
785
786         return list;
787     }
788
789     /**
790      * From com.google.android.gm.ComposeActivity
791      * Implements special address cleanup rules:
792      * The first space key entry following an "@" symbol that is followed by any combination
793      * of letters and symbols, including one+ dots and zero commas, should insert an extra
794      * comma (followed by the space).
795      */
796     private static InputFilter[] sRecipientFilters = new InputFilter[] { new InputFilter() {
797
798         public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
799                 int dstart, int dend) {
800
801             // quick check - did they enter a single space?
802             if (end-start != 1 || source.charAt(start) != ' ') {
803                 return null;
804             }
805
806             // determine if the characters before the new space fit the pattern
807             // follow backwards and see if we find a comma, dot, or @
808             int scanBack = dstart;
809             boolean dotFound = false;
810             while (scanBack > 0) {
811                 char c = dest.charAt(--scanBack);
812                 switch (c) {
813                     case '.':
814                         dotFound = true;    // one or more dots are req'd
815                         break;
816                     case ',':
817                         return null;
818                     case '@':
819                         if (!dotFound) {
820                             return null;
821                         }
822                         // we have found a comma-insert case.  now just do it
823                         // in the least expensive way we can.
824                         if (source instanceof Spanned) {
825                             SpannableStringBuilder sb = new SpannableStringBuilder(",");
826                             sb.append(source);
827                             return sb;
828                         } else {
829                             return ", ";
830                         }
831                     default:
832                         // just keep going
833                 }
834             }
835
836             // no termination cases were found, so don't edit the input
837             return null;
838         }
839     }};
840
841     private void initFromIntent(Intent intent) {
842         String title = intent.getStringExtra(Events.TITLE);
843         if (title != null) {
844             mTitleTextView.setText(title);
845         }
846
847         String location = intent.getStringExtra(Events.EVENT_LOCATION);
848         if (location != null) {
849             mLocationTextView.setText(location);
850         }
851
852         String description = intent.getStringExtra(Events.DESCRIPTION);
853         if (description != null) {
854             mDescriptionTextView.setText(description);
855         }
856
857         int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
858         if (availability != -1) {
859             mAvailabilitySpinner.setSelection(availability);
860         }
861
862         int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
863         if (visibility != -1) {
864             mVisibilitySpinner.setSelection(visibility);
865         }
866
867         String rrule = intent.getStringExtra(Events.RRULE);
868         if (rrule != null) {
869             mRrule = rrule;
870             mEventRecurrence.parse(rrule);
871         }
872     }
873
874     @Override
875     protected void onResume() {
876         super.onResume();
877
878         if (mUri != null) {
879             if (mEventCursor == null || mEventCursor.getCount() == 0) {
880                 // The cursor is empty. This can happen if the event was deleted.
881                 finish();
882                 return;
883             }
884         }
885
886         if (mEventCursor != null) {
887             Cursor cursor = mEventCursor;
888             cursor.moveToFirst();
889
890             mRrule = cursor.getString(EVENT_INDEX_RRULE);
891             String title = cursor.getString(EVENT_INDEX_TITLE);
892             String description = cursor.getString(EVENT_INDEX_DESCRIPTION);
893             String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
894             int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY);
895             int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY);
896             if (visibility > 0) {
897                 // For now we the array contains the values 0, 2, and 3. We subtract one to match.
898                 visibility--;
899             }
900
901             if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) {
902                 // If this event has not been synced, then don't allow deleting
903                 // or changing a single instance.
904                 mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
905                 mEventRecurrence.parse(mRrule);
906
907                 // If we haven't synced this repeating event yet, then don't
908                 // allow the user to change just one instance.
909                 int itemIndex = 0;
910                 CharSequence[] items;
911                 if (mSyncId == null) {
912                     items = new CharSequence[2];
913                 } else {
914                     items = new CharSequence[3];
915                     items[itemIndex++] = getText(R.string.modify_event);
916                 }
917                 items[itemIndex++] = getText(R.string.modify_all);
918                 items[itemIndex++] = getText(R.string.modify_all_following);
919
920                 // Display the modification dialog.
921                 new AlertDialog.Builder(this)
922                         .setOnCancelListener(new OnCancelListener() {
923                             public void onCancel(DialogInterface dialog) {
924                                 finish();
925                             }
926                         })
927                         .setTitle(R.string.edit_event_label)
928                         .setItems(items, new OnClickListener() {
929                             public void onClick(DialogInterface dialog, int which) {
930                                 if (which == 0) {
931                                     mModification =
932                                             (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED;
933                                 } else if (which == 1) {
934                                     mModification =
935                                         (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL;
936                                 } else if (which == 2) {
937                                     mModification = MODIFY_ALL_FOLLOWING;
938                                 }
939
940                                 // If we are modifying all the events in a
941                                 // series then disable and ignore the date.
942                                 if (mModification == MODIFY_ALL) {
943                                     mStartDateButton.setEnabled(false);
944                                     mEndDateButton.setEnabled(false);
945                                 } else if (mModification == MODIFY_SELECTED) {
946                                     mRepeatsSpinner.setEnabled(false);
947                                 }
948                             }
949                         })
950                         .show();
951             }
952
953             mTitleTextView.setText(title);
954             mLocationTextView.setText(location);
955             mDescriptionTextView.setText(description);
956             mAvailabilitySpinner.setSelection(availability);
957             mVisibilitySpinner.setSelection(visibility);
958
959             // This is an existing event so hide the calendar spinner
960             // since we can't change the calendar.
961             View calendarGroup = findViewById(R.id.calendar_group);
962             calendarGroup.setVisibility(View.GONE);
963         } else {
964             // New event
965             if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) {
966                 mStartTime.setToNow();
967
968                 // Round the time to the nearest half hour.
969                 mStartTime.second = 0;
970                 int minute = mStartTime.minute;
971                 if (minute > 0 && minute <= 30) {
972                     mStartTime.minute = 30;
973                 } else {
974                     mStartTime.minute = 0;
975                     mStartTime.hour += 1;
976                 }
977
978                 long startMillis = mStartTime.normalize(true /* ignore isDst */);
979                 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
980             }
981
982             // New event - set the default reminder
983             if (mDefaultReminderMinutes != 0) {
984                 addReminder(this, this, mReminderItems, mReminderValues,
985                         mReminderLabels, mDefaultReminderMinutes);
986             }
987
988             // Hide delete button
989             mDeleteButton.setVisibility(View.GONE);
990         }
991
992         updateRemindersVisibility();
993         populateWhen();
994         populateRepeats();
995     }
996
997     @Override
998     public boolean onCreateOptionsMenu(Menu menu) {
999         MenuItem item;
1000         item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
1001                 R.string.add_new_reminder);
1002         item.setIcon(R.drawable.ic_menu_reminder);
1003         item.setAlphabeticShortcut('r');
1004
1005         item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
1006                 R.string.edit_event_show_extra_options);
1007         item.setIcon(R.drawable.ic_menu_show_list);
1008         item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
1009                 R.string.edit_event_hide_extra_options);
1010         item.setIcon(R.drawable.ic_menu_show_list);
1011
1012         return super.onCreateOptionsMenu(menu);
1013     }
1014
1015     @Override
1016     public boolean onPrepareOptionsMenu(Menu menu) {
1017         if (mReminderItems.size() < MAX_REMINDERS) {
1018             menu.setGroupVisible(MENU_GROUP_REMINDER, true);
1019             menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
1020         } else {
1021             menu.setGroupVisible(MENU_GROUP_REMINDER, false);
1022             menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
1023         }
1024
1025         if (mExtraOptions.getVisibility() == View.VISIBLE) {
1026             menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
1027             menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
1028         } else {
1029             menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
1030             menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
1031         }
1032
1033         return super.onPrepareOptionsMenu(menu);
1034     }
1035
1036     private void addReminder() {
1037         // TODO: when adding a new reminder, make it different from the
1038         // last one in the list (if any).
1039         if (mDefaultReminderMinutes == 0) {
1040             addReminder(this, this, mReminderItems, mReminderValues,
1041                     mReminderLabels, 10 /* minutes */);
1042         } else {
1043             addReminder(this, this, mReminderItems, mReminderValues,
1044                     mReminderLabels, mDefaultReminderMinutes);
1045         }
1046         updateRemindersVisibility();
1047     }
1048
1049     @Override
1050     public boolean onOptionsItemSelected(MenuItem item) {
1051         switch (item.getItemId()) {
1052         case MENU_ADD_REMINDER:
1053             addReminder();
1054             return true;
1055         case MENU_SHOW_EXTRA_OPTIONS:
1056             mExtraOptions.setVisibility(View.VISIBLE);
1057             return true;
1058         case MENU_HIDE_EXTRA_OPTIONS:
1059             mExtraOptions.setVisibility(View.GONE);
1060             return true;
1061         }
1062         return super.onOptionsItemSelected(item);
1063     }
1064
1065     @Override
1066     public boolean onKeyDown(int keyCode, KeyEvent event) {
1067         switch (keyCode) {
1068             case KeyEvent.KEYCODE_BACK:
1069                 // If we are creating a new event, do not create it if the
1070                 // title, location and description are all empty, in order to
1071                 // prevent accidental "no subject" event creations.
1072                 if (mUri != null || !isEmpty()) {
1073                     if (!save()) {
1074                         // We cannot exit this activity because the calendars
1075                         // are still loading.
1076                         return true;
1077                     }
1078                 }
1079                 break;
1080         }
1081
1082         return super.onKeyDown(keyCode, event);
1083     }
1084
1085     private void populateWhen() {
1086         long startMillis = mStartTime.toMillis(false /* use isDst */);
1087         long endMillis = mEndTime.toMillis(false /* use isDst */);
1088         setDate(mStartDateButton, startMillis);
1089         setDate(mEndDateButton, endMillis);
1090
1091         setTime(mStartTimeButton, startMillis);
1092         setTime(mEndTimeButton, endMillis);
1093
1094         mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
1095         mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
1096
1097         mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
1098         mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
1099     }
1100
1101     private void populateRepeats() {
1102         Time time = mStartTime;
1103         Resources r = getResources();
1104         int resource = android.R.layout.simple_spinner_item;
1105
1106         String[] days = new String[] {
1107             DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
1108             DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
1109             DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
1110             DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
1111             DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
1112             DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
1113             DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
1114         };
1115         String[] ordinals = r.getStringArray(R.array.ordinal_labels);
1116
1117         // Only display "Custom" in the spinner if the device does not support the
1118         // recurrence functionality of the event. Only display every weekday if
1119         // the event starts on a weekday.
1120         boolean isCustomRecurrence = isCustomRecurrence();
1121         boolean isWeekdayEvent = isWeekdayEvent();
1122
1123         ArrayList<String> repeatArray = new ArrayList<String>(0);
1124         ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
1125
1126         repeatArray.add(r.getString(R.string.does_not_repeat));
1127         recurrenceIndexes.add(DOES_NOT_REPEAT);
1128
1129         repeatArray.add(r.getString(R.string.daily));
1130         recurrenceIndexes.add(REPEATS_DAILY);
1131
1132         if (isWeekdayEvent) {
1133             repeatArray.add(r.getString(R.string.every_weekday));
1134             recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
1135         }
1136
1137         String format = r.getString(R.string.weekly);
1138         repeatArray.add(String.format(format, time.format("%A")));
1139         recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
1140
1141         // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
1142         int dayNumber = (time.monthDay - 1) / 7;
1143         format = r.getString(R.string.monthly_on_day_count);
1144         repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
1145         recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
1146
1147         format = r.getString(R.string.monthly_on_day);
1148         repeatArray.add(String.format(format, time.monthDay));
1149         recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
1150
1151         long when = time.toMillis(false);
1152         format = r.getString(R.string.yearly);
1153         int flags = 0;
1154         if (DateFormat.is24HourFormat(this)) {
1155             flags |= DateUtils.FORMAT_24HOUR;
1156         }
1157         repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
1158         recurrenceIndexes.add(REPEATS_YEARLY);
1159
1160         if (isCustomRecurrence) {
1161             repeatArray.add(r.getString(R.string.custom));
1162             recurrenceIndexes.add(REPEATS_CUSTOM);
1163         }
1164         mRecurrenceIndexes = recurrenceIndexes;
1165
1166         int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
1167         if (mRrule != null) {
1168             if (isCustomRecurrence) {
1169                 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
1170             } else {
1171                 switch (mEventRecurrence.freq) {
1172                     case EventRecurrence.DAILY:
1173                         position = recurrenceIndexes.indexOf(REPEATS_DAILY);
1174                         break;
1175                     case EventRecurrence.WEEKLY:
1176                         if (mEventRecurrence.repeatsOnEveryWeekDay()) {
1177                             position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
1178                         } else {
1179                             position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
1180                         }
1181                         break;
1182                     case EventRecurrence.MONTHLY:
1183                         if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1184                             position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
1185                         } else {
1186                             position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
1187                         }
1188                         break;
1189                     case EventRecurrence.YEARLY:
1190                         position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
1191                         break;
1192                 }
1193             }
1194         }
1195         ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
1196         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1197         mRepeatsSpinner.setAdapter(adapter);
1198         mRepeatsSpinner.setSelection(position);
1199     }
1200
1201     // Adds a reminder to the displayed list of reminders.
1202     // Returns true if successfully added reminder, false if no reminders can
1203     // be added.
1204     static boolean addReminder(Activity activity, View.OnClickListener listener,
1205             ArrayList<LinearLayout> items, ArrayList<Integer> values,
1206             ArrayList<String> labels, int minutes) {
1207
1208         if (items.size() >= MAX_REMINDERS) {
1209             return false;
1210         }
1211
1212         LayoutInflater inflater = activity.getLayoutInflater();
1213         LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
1214         LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
1215         parent.addView(reminderItem);
1216
1217         Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
1218         Resources res = activity.getResources();
1219         spinner.setPrompt(res.getString(R.string.reminders_label));
1220         int resource = android.R.layout.simple_spinner_item;
1221         ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
1222         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1223         spinner.setAdapter(adapter);
1224
1225         ImageButton reminderRemoveButton;
1226         reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
1227         reminderRemoveButton.setOnClickListener(listener);
1228
1229         int index = findMinutesInReminderList(values, minutes);
1230         spinner.setSelection(index);
1231         items.add(reminderItem);
1232
1233         return true;
1234     }
1235
1236     static void addMinutesToList(Context context, ArrayList<Integer> values,
1237             ArrayList<String> labels, int minutes) {
1238         int index = values.indexOf(minutes);
1239         if (index != -1) {
1240             return;
1241         }
1242
1243         // The requested "minutes" does not exist in the list, so insert it
1244         // into the list.
1245
1246         String label = constructReminderLabel(context, minutes, false);
1247         int len = values.size();
1248         for (int i = 0; i < len; i++) {
1249             if (minutes < values.get(i)) {
1250                 values.add(i, minutes);
1251                 labels.add(i, label);
1252                 return;
1253             }
1254         }
1255
1256         values.add(minutes);
1257         labels.add(len, label);
1258     }
1259
1260     /**
1261      * Finds the index of the given "minutes" in the "values" list.
1262      *
1263      * @param values the list of minutes corresponding to the spinner choices
1264      * @param minutes the minutes to search for in the values list
1265      * @return the index of "minutes" in the "values" list
1266      */
1267     private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
1268         int index = values.indexOf(minutes);
1269         if (index == -1) {
1270             // This should never happen.
1271             Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
1272             return 0;
1273         }
1274         return index;
1275     }
1276
1277     // Constructs a label given an arbitrary number of minutes.  For example,
1278     // if the given minutes is 63, then this returns the string "63 minutes".
1279     // As another example, if the given minutes is 120, then this returns
1280     // "2 hours".
1281     static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
1282         Resources resources = context.getResources();
1283         int value, resId;
1284
1285         if (minutes % 60 != 0) {
1286             value = minutes;
1287             if (abbrev) {
1288                 resId = R.plurals.Nmins;
1289             } else {
1290                 resId = R.plurals.Nminutes;
1291             }
1292         } else if (minutes % (24 * 60) != 0) {
1293             value = minutes / 60;
1294             resId = R.plurals.Nhours;
1295         } else {
1296             value = minutes / ( 24 * 60);
1297             resId = R.plurals.Ndays;
1298         }
1299
1300         String format = resources.getQuantityString(resId, value);
1301         return String.format(format, value);
1302     }
1303
1304     private void updateRemindersVisibility() {
1305         if (mReminderItems.size() == 0) {
1306             mRemindersSeparator.setVisibility(View.GONE);
1307             mRemindersContainer.setVisibility(View.GONE);
1308         } else {
1309             mRemindersSeparator.setVisibility(View.VISIBLE);
1310             mRemindersContainer.setVisibility(View.VISIBLE);
1311         }
1312     }
1313
1314     private void setDate(TextView view, long millis) {
1315         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
1316                 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
1317                 DateUtils.FORMAT_ABBREV_WEEKDAY;
1318         view.setText(DateUtils.formatDateTime(this, millis, flags));
1319     }
1320
1321     private void setTime(TextView view, long millis) {
1322         int flags = DateUtils.FORMAT_SHOW_TIME;
1323         if (DateFormat.is24HourFormat(this)) {
1324             flags |= DateUtils.FORMAT_24HOUR;
1325         }
1326         view.setText(DateUtils.formatDateTime(this, millis, flags));
1327     }
1328
1329     // Saves the event.  Returns true if it is okay to exit this activity.
1330     private boolean save() {
1331         boolean forceSaveReminders = false;
1332
1333         // If we are creating a new event, then make sure we wait until the
1334         // query to fetch the list of calendars has finished.
1335         if (mEventCursor == null) {
1336             if (!mCalendarsQueryComplete) {
1337                 // Wait for the calendars query to finish.
1338                 if (mLoadingCalendarsDialog == null) {
1339                     // Create the progress dialog
1340                     mLoadingCalendarsDialog = ProgressDialog.show(this,
1341                             getText(R.string.loading_calendars_title),
1342                             getText(R.string.loading_calendars_message),
1343                             true, true, this);
1344                     mSaveAfterQueryComplete = true;
1345                 }
1346                 return false;
1347             }
1348
1349             // Avoid creating a new event if the calendars cursor is empty. This
1350             // shouldn't ever happen since the setup wizard should ensure the user
1351             // has a calendar.
1352             if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0) {
1353                 Log.w("Cal", "The calendars table does not contain any calendars."
1354                         + " New event was not created.");
1355                 return true;
1356             }
1357             Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
1358         } else {
1359             Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
1360         }
1361
1362         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1363         int eventIdIndex = -1;
1364
1365         ContentValues values = getContentValuesFromUi();
1366         Uri uri = mUri;
1367
1368         // Update the "hasAlarm" field for the event
1369         ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
1370                 mReminderValues);
1371         int len = reminderMinutes.size();
1372         values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
1373
1374         // For recurring events, we must make sure that we use duration rather
1375         // than dtend.
1376         if (uri == null) {
1377             // Create new event with new contents
1378             addRecurrenceRule(values);
1379             eventIdIndex = ops.size();
1380             Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1381             ops.add(b.build());
1382             forceSaveReminders = true;
1383
1384         } else if (mRrule == null) {
1385             // Modify contents of a non-repeating event
1386             addRecurrenceRule(values);
1387             checkTimeDependentFields(values);
1388             ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1389
1390         } else if (mInitialValues.getAsString(Events.RRULE) == null) {
1391             // This event was changed from a non-repeating event to a
1392             // repeating event.
1393             addRecurrenceRule(values);
1394             values.remove(Events.DTEND);
1395             ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1396
1397         } else if (mModification == MODIFY_SELECTED) {
1398             // Modify contents of the current instance of repeating event
1399
1400             // Create a recurrence exception
1401             long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1402             values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
1403             values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
1404             boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1405             values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
1406
1407             eventIdIndex = ops.size();
1408             Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1409             ops.add(b.build());
1410             forceSaveReminders = true;
1411
1412         } else if (mModification == MODIFY_ALL_FOLLOWING) {
1413             // Modify this instance and all future instances of repeating event
1414             addRecurrenceRule(values);
1415
1416             if (mRrule == null) {
1417                 // We've changed a recurring event to a non-recurring event.
1418                 // If the event we are editing is the first in the series,
1419                 // then delete the whole series.  Otherwise, update the series
1420                 // to end at the new start time.
1421                 if (isFirstEventInSeries()) {
1422                     ops.add(ContentProviderOperation.newDelete(uri).build());
1423                 } else {
1424                     // Update the current repeating event to end at the new
1425                     // start time.
1426                     updatePastEvents(ops, uri);
1427                 }
1428                 eventIdIndex = ops.size();
1429                 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1430                         .build());
1431             } else {
1432                 if (isFirstEventInSeries()) {
1433                     checkTimeDependentFields(values);
1434                     values.remove(Events.DTEND);
1435                     Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
1436                     ops.add(b.build());
1437                 } else {
1438                     // Update the current repeating event to end at the new
1439                     // start time.
1440                     updatePastEvents(ops, uri);
1441
1442                     // Create a new event with the user-modified fields
1443                     values.remove(Events.DTEND);
1444                     eventIdIndex = ops.size();
1445                     ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
1446                             values).build());
1447                 }
1448             }
1449             forceSaveReminders = true;
1450
1451         } else if (mModification == MODIFY_ALL) {
1452
1453             // Modify all instances of repeating event
1454             addRecurrenceRule(values);
1455
1456             if (mRrule == null) {
1457                 // We've changed a recurring event to a non-recurring event.
1458                 // Delete the whole series and replace it with a new
1459                 // non-recurring event.
1460                 ops.add(ContentProviderOperation.newDelete(uri).build());
1461
1462                 eventIdIndex = ops.size();
1463                 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1464                         .build());
1465                 forceSaveReminders = true;
1466             } else {
1467                 checkTimeDependentFields(values);
1468                 values.remove(Events.DTEND);
1469                 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1470             }
1471         }
1472
1473         if (eventIdIndex != -1) {
1474             saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
1475                     forceSaveReminders);
1476         } else if (uri != null) {
1477             long eventId = ContentUris.parseId(uri);
1478             saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
1479                     forceSaveReminders);
1480         }
1481
1482         if (eventIdIndex != -1 || uri != null) {
1483             Editable attendeesText = mAttendeesList.getText();
1484             // Hit the content provider only if the user has changed it
1485             if (!mOriginalAttendees.equals(attendeesText.toString())) {
1486                 // TODO we could do a diff and modify the rows only as needed
1487                 // Delete all the existing attendees for this event
1488                 Builder b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
1489
1490                 long eventId = -1;
1491                 if (eventIdIndex == -1) {
1492                     eventId = ContentUris.parseId(uri);
1493                     String[] args = new String[] {
1494                         Long.toString(eventId)
1495                     };
1496                     b.withSelection(ATTENDEES_WHERE, args);
1497                 } else {
1498                     // Delete all the existing reminders for this event
1499                     b.withSelection(ATTENDEES_WHERE, new String[1]);
1500                     b.withSelectionBackReference(0, eventIdIndex);
1501                 }
1502                 ops.add(b.build());
1503
1504                 if (attendeesText.length() > 0) {
1505                     Rfc822Token[] attendees = getAddressesFromList(mAttendeesList);
1506                     // Insert the attendees
1507                     for (Rfc822Token attendee : attendees) {
1508                         values.clear();
1509                         values.put(Attendees.ATTENDEE_NAME, attendee.getName());
1510                         values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
1511                         values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
1512                         values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1513                         values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
1514
1515                         if (eventIdIndex != -1) {
1516                             b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1517                                     .withValues(values);
1518                             b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1519                         } else {
1520                             values.put(Attendees.EVENT_ID, eventId);
1521                             b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1522                                     .withValues(values);
1523                         }
1524                         ops.add(b.build());
1525                     }
1526                 }
1527             }
1528         }
1529
1530         try {
1531             // TODO Move this to background thread
1532             ContentProviderResult[] results =
1533                 getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
1534             if (DEBUG) {
1535                 for (int i = 0; i < results.length; i++) {
1536                     Log.v(TAG, "results = " + results[i].toString());
1537                 }
1538             }
1539         } catch (RemoteException e) {
1540             // TODO Auto-generated catch block
1541             e.printStackTrace();
1542         } catch (OperationApplicationException e) {
1543             // TODO Auto-generated catch block
1544             e.printStackTrace();
1545         }
1546
1547         return true;
1548     }
1549
1550     private boolean isFirstEventInSeries() {
1551         int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
1552         long start = mEventCursor.getLong(dtStart);
1553         return start == mStartTime.toMillis(true);
1554     }
1555
1556     private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) {
1557         long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1558         String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
1559         boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1560         String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
1561         mEventRecurrence.parse(oldRrule);
1562
1563         Time untilTime = new Time();
1564         long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1565         ContentValues oldValues = new ContentValues();
1566
1567         // The "until" time must be in UTC time in order for Google calendar
1568         // to display it properly.  For all-day events, the "until" time string
1569         // must include just the date field, and not the time field.  The
1570         // repeating events repeat up to and including the "until" time.
1571         untilTime.timezone = Time.TIMEZONE_UTC;
1572
1573         // Subtract one second from the old begin time to get the new
1574         // "until" time.
1575         untilTime.set(begin - 1000);  // subtract one second (1000 millis)
1576         if (allDay) {
1577             untilTime.hour = 0;
1578             untilTime.minute = 0;
1579             untilTime.second = 0;
1580             untilTime.allDay = true;
1581             untilTime.normalize(false);
1582
1583             // For all-day events, the duration must be in days, not seconds.
1584             // Otherwise, Google Calendar will (mistakenly) change this event
1585             // into a non-all-day event.
1586             int len = oldDuration.length();
1587             if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
1588                 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
1589                 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1590                 oldDuration = "P" + days + "D";
1591             }
1592         }
1593         mEventRecurrence.until = untilTime.format2445();
1594
1595         oldValues.put(Events.DTSTART, oldStartMillis);
1596         oldValues.put(Events.DURATION, oldDuration);
1597         oldValues.put(Events.RRULE, mEventRecurrence.toString());
1598         Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
1599         ops.add(b.build());
1600     }
1601
1602     private void checkTimeDependentFields(ContentValues values) {
1603         long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1604         long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
1605         boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1606         String oldRrule = mInitialValues.getAsString(Events.RRULE);
1607         String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
1608
1609         long newBegin = values.getAsLong(Events.DTSTART);
1610         long newEnd = values.getAsLong(Events.DTEND);
1611         boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
1612         String newRrule = values.getAsString(Events.RRULE);
1613         String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1614
1615         // If none of the time-dependent fields changed, then remove them.
1616         if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
1617                 && TextUtils.equals(oldRrule, newRrule)
1618                 && TextUtils.equals(oldTimezone, newTimezone)) {
1619             values.remove(Events.DTSTART);
1620             values.remove(Events.DTEND);
1621             values.remove(Events.DURATION);
1622             values.remove(Events.ALL_DAY);
1623             values.remove(Events.RRULE);
1624             values.remove(Events.EVENT_TIMEZONE);
1625             return;
1626         }
1627
1628         if (oldRrule == null || newRrule == null) {
1629             return;
1630         }
1631
1632         // If we are modifying all events then we need to set DTSTART to the
1633         // start time of the first event in the series, not the current
1634         // date and time.  If the start time of the event was changed
1635         // (from, say, 3pm to 4pm), then we want to add the time difference
1636         // to the start time of the first event in the series (the DTSTART
1637         // value).  If we are modifying one instance or all following instances,
1638         // then we leave the DTSTART field alone.
1639         if (mModification == MODIFY_ALL) {
1640             long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1641             if (oldBegin != newBegin) {
1642                 // The user changed the start time of this event
1643                 long offset = newBegin - oldBegin;
1644                 oldStartMillis += offset;
1645             }
1646             values.put(Events.DTSTART, oldStartMillis);
1647         }
1648     }
1649
1650     static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
1651             ArrayList<Integer> reminderValues) {
1652         int len = reminderItems.size();
1653         ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
1654         for (int index = 0; index < len; index++) {
1655             LinearLayout layout = reminderItems.get(index);
1656             Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
1657             int minutes = reminderValues.get(spinner.getSelectedItemPosition());
1658             reminderMinutes.add(minutes);
1659         }
1660         return reminderMinutes;
1661     }
1662
1663     /**
1664      * Saves the reminders, if they changed.  Returns true if the database
1665      * was updated.
1666      *
1667      * @param ops the array of ContentProviderOperations
1668      * @param eventId the id of the event whose reminders are being updated
1669      * @param reminderMinutes the array of reminders set by the user
1670      * @param originalMinutes the original array of reminders
1671      * @param forceSave if true, then save the reminders even if they didn't
1672      *   change
1673      * @return true if the database was updated
1674      */
1675     static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
1676             ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
1677             boolean forceSave) {
1678         // If the reminders have not changed, then don't update the database
1679         if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1680             return false;
1681         }
1682
1683         // Delete all the existing reminders for this event
1684         String where = Reminders.EVENT_ID + "=?";
1685         String[] args = new String[] { Long.toString(eventId) };
1686         Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1687         b.withSelection(where, args);
1688         ops.add(b.build());
1689
1690         ContentValues values = new ContentValues();
1691         int len = reminderMinutes.size();
1692
1693         // Insert the new reminders, if any
1694         for (int i = 0; i < len; i++) {
1695             int minutes = reminderMinutes.get(i);
1696
1697             values.clear();
1698             values.put(Reminders.MINUTES, minutes);
1699             values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1700             values.put(Reminders.EVENT_ID, eventId);
1701             b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1702             ops.add(b.build());
1703         }
1704         return true;
1705     }
1706
1707     static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
1708             int eventIdIndex, ArrayList<Integer> reminderMinutes,
1709             ArrayList<Integer> originalMinutes, boolean forceSave) {
1710         // If the reminders have not changed, then don't update the database
1711         if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1712             return false;
1713         }
1714
1715         // Delete all the existing reminders for this event
1716         Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1717         b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
1718         b.withSelectionBackReference(0, eventIdIndex);
1719         ops.add(b.build());
1720
1721         ContentValues values = new ContentValues();
1722         int len = reminderMinutes.size();
1723
1724         // Insert the new reminders, if any
1725         for (int i = 0; i < len; i++) {
1726             int minutes = reminderMinutes.get(i);
1727
1728             values.clear();
1729             values.put(Reminders.MINUTES, minutes);
1730             values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1731             b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1732             b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1733             ops.add(b.build());
1734         }
1735         return true;
1736     }
1737
1738     private void addRecurrenceRule(ContentValues values) {
1739         updateRecurrenceRule();
1740
1741         if (mRrule == null) {
1742             return;
1743         }
1744
1745         values.put(Events.RRULE, mRrule);
1746         long end = mEndTime.toMillis(true /* ignore dst */);
1747         long start = mStartTime.toMillis(true /* ignore dst */);
1748         String duration;
1749
1750         boolean isAllDay = mAllDayCheckBox.isChecked();
1751         if (isAllDay) {
1752             long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
1753             duration = "P" + days + "D";
1754         } else {
1755             long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
1756             duration = "P" + seconds + "S";
1757         }
1758         values.put(Events.DURATION, duration);
1759     }
1760
1761     private void updateRecurrenceRule() {
1762         int position = mRepeatsSpinner.getSelectedItemPosition();
1763         int selection = mRecurrenceIndexes.get(position);
1764
1765         if (selection == DOES_NOT_REPEAT) {
1766             mRrule = null;
1767             return;
1768         } else if (selection == REPEATS_CUSTOM) {
1769             // Keep custom recurrence as before.
1770             return;
1771         } else if (selection == REPEATS_DAILY) {
1772             mEventRecurrence.freq = EventRecurrence.DAILY;
1773         } else if (selection == REPEATS_EVERY_WEEKDAY) {
1774             mEventRecurrence.freq = EventRecurrence.WEEKLY;
1775             int dayCount = 5;
1776             int[] byday = new int[dayCount];
1777             int[] bydayNum = new int[dayCount];
1778
1779             byday[0] = EventRecurrence.MO;
1780             byday[1] = EventRecurrence.TU;
1781             byday[2] = EventRecurrence.WE;
1782             byday[3] = EventRecurrence.TH;
1783             byday[4] = EventRecurrence.FR;
1784             for (int day = 0; day < dayCount; day++) {
1785                 bydayNum[day] = 0;
1786             }
1787
1788             mEventRecurrence.byday = byday;
1789             mEventRecurrence.bydayNum = bydayNum;
1790             mEventRecurrence.bydayCount = dayCount;
1791         } else if (selection == REPEATS_WEEKLY_ON_DAY) {
1792             mEventRecurrence.freq = EventRecurrence.WEEKLY;
1793             int[] days = new int[1];
1794             int dayCount = 1;
1795             int[] dayNum = new int[dayCount];
1796
1797             days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1798             // not sure why this needs to be zero, but set it for now.
1799             dayNum[0] = 0;
1800
1801             mEventRecurrence.byday = days;
1802             mEventRecurrence.bydayNum = dayNum;
1803             mEventRecurrence.bydayCount = dayCount;
1804         } else if (selection == REPEATS_MONTHLY_ON_DAY) {
1805             mEventRecurrence.freq = EventRecurrence.MONTHLY;
1806             mEventRecurrence.bydayCount = 0;
1807             mEventRecurrence.bymonthdayCount = 1;
1808             int[] bymonthday = new int[1];
1809             bymonthday[0] = mStartTime.monthDay;
1810             mEventRecurrence.bymonthday = bymonthday;
1811         } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
1812             mEventRecurrence.freq = EventRecurrence.MONTHLY;
1813             mEventRecurrence.bydayCount = 1;
1814             mEventRecurrence.bymonthdayCount = 0;
1815
1816             int[] byday = new int[1];
1817             int[] bydayNum = new int[1];
1818             // Compute the week number (for example, the "2nd" Monday)
1819             int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
1820             if (dayCount == 5) {
1821                 dayCount = -1;
1822             }
1823             bydayNum[0] = dayCount;
1824             byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1825             mEventRecurrence.byday = byday;
1826             mEventRecurrence.bydayNum = bydayNum;
1827         } else if (selection == REPEATS_YEARLY) {
1828             mEventRecurrence.freq = EventRecurrence.YEARLY;
1829         }
1830
1831         // Set the week start day.
1832         mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
1833         mRrule = mEventRecurrence.toString();
1834     }
1835
1836     private ContentValues getContentValuesFromUi() {
1837         String title = mTitleTextView.getText().toString();
1838         boolean isAllDay = mAllDayCheckBox.isChecked();
1839         String location = mLocationTextView.getText().toString();
1840         String description = mDescriptionTextView.getText().toString();
1841
1842         ContentValues values = new ContentValues();
1843
1844         String timezone = null;
1845         long startMillis;
1846         long endMillis;
1847         long calendarId;
1848         if (isAllDay) {
1849             // Reset start and end time, increment the monthDay by 1, and set
1850             // the timezone to UTC, as required for all-day events.
1851             timezone = Time.TIMEZONE_UTC;
1852             mStartTime.hour = 0;
1853             mStartTime.minute = 0;
1854             mStartTime.second = 0;
1855             mStartTime.timezone = timezone;
1856             startMillis = mStartTime.normalize(true);
1857
1858             mEndTime.hour = 0;
1859             mEndTime.minute = 0;
1860             mEndTime.second = 0;
1861             mEndTime.monthDay++;
1862             mEndTime.timezone = timezone;
1863             endMillis = mEndTime.normalize(true);
1864
1865             if (mEventCursor == null) {
1866                 // This is a new event
1867                 calendarId = mCalendarsSpinner.getSelectedItemId();
1868             } else {
1869                 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1870             }
1871         } else {
1872             startMillis = mStartTime.toMillis(true);
1873             endMillis = mEndTime.toMillis(true);
1874             if (mEventCursor != null) {
1875                 // This is an existing event
1876                 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
1877
1878                 // The timezone might be null if we are changing an existing
1879                 // all-day event to a non-all-day event.  We need to assign
1880                 // a timezone to the non-all-day event.
1881                 if (TextUtils.isEmpty(timezone)) {
1882                     timezone = TimeZone.getDefault().getID();
1883                 }
1884                 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1885             } else {
1886                 // This is a new event
1887                 calendarId = mCalendarsSpinner.getSelectedItemId();
1888
1889                 // The timezone for a new event is the currently displayed
1890                 // timezone, NOT the timezone of the containing calendar.
1891                 timezone = TimeZone.getDefault().getID();
1892             }
1893         }
1894
1895         values.put(Events.CALENDAR_ID, calendarId);
1896         values.put(Events.EVENT_TIMEZONE, timezone);
1897         values.put(Events.TITLE, title);
1898         values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
1899         values.put(Events.DTSTART, startMillis);
1900         values.put(Events.DTEND, endMillis);
1901         values.put(Events.DESCRIPTION, description);
1902         values.put(Events.EVENT_LOCATION, location);
1903         values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
1904
1905         int visibility = mVisibilitySpinner.getSelectedItemPosition();
1906         if (visibility > 0) {
1907             // For now we the array contains the values 0, 2, and 3. We add one to match.
1908             visibility++;
1909         }
1910         values.put(Events.VISIBILITY, visibility);
1911
1912         return values;
1913     }
1914
1915     private boolean isEmpty() {
1916         String title = mTitleTextView.getText().toString();
1917         if (title.length() > 0) {
1918             return false;
1919         }
1920
1921         String location = mLocationTextView.getText().toString();
1922         if (location.length() > 0) {
1923             return false;
1924         }
1925
1926         String description = mDescriptionTextView.getText().toString();
1927         if (description.length() > 0) {
1928             return false;
1929         }
1930
1931         return true;
1932     }
1933
1934     private boolean isCustomRecurrence() {
1935
1936         if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
1937             return true;
1938         }
1939
1940         if (mEventRecurrence.freq == 0) {
1941             return false;
1942         }
1943
1944         switch (mEventRecurrence.freq) {
1945         case EventRecurrence.DAILY:
1946             return false;
1947         case EventRecurrence.WEEKLY:
1948             if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
1949                 return false;
1950             } else if (mEventRecurrence.bydayCount == 1) {
1951                 return false;
1952             }
1953             break;
1954         case EventRecurrence.MONTHLY:
1955             if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1956                 return false;
1957             } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
1958                 return false;
1959             }
1960             break;
1961         case EventRecurrence.YEARLY:
1962             return false;
1963         }
1964
1965         return true;
1966     }
1967
1968     private boolean isWeekdayEvent() {
1969         if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {
1970             return true;
1971         }
1972         return false;
1973     }
1974 }