OSDN Git Service

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