OSDN Git Service

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