OSDN Git Service

Merge "b/2492707 If all guests have no response will be called 'Guests' not 'Maybe'"
[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) {
1022                     // We are already on a half hour increment
1023                 } else if (minute > 0 && minute <= 30) {
1024                     mStartTime.minute = 30;
1025                 } else {
1026                     mStartTime.minute = 0;
1027                     mStartTime.hour += 1;
1028                 }
1029
1030                 long startMillis = mStartTime.normalize(true /* ignore isDst */);
1031                 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
1032             }
1033
1034             // Hide delete button
1035             mDeleteButton.setVisibility(View.GONE);
1036         }
1037
1038         updateRemindersVisibility();
1039         populateWhen();
1040         populateRepeats();
1041     }
1042
1043     @Override
1044     public boolean onCreateOptionsMenu(Menu menu) {
1045         MenuItem item;
1046         item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
1047                 R.string.add_new_reminder);
1048         item.setIcon(R.drawable.ic_menu_reminder);
1049         item.setAlphabeticShortcut('r');
1050
1051         item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
1052                 R.string.edit_event_show_extra_options);
1053         item.setIcon(R.drawable.ic_menu_show_list);
1054         item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
1055                 R.string.edit_event_hide_extra_options);
1056         item.setIcon(R.drawable.ic_menu_show_list);
1057
1058         return super.onCreateOptionsMenu(menu);
1059     }
1060
1061     @Override
1062     public boolean onPrepareOptionsMenu(Menu menu) {
1063         if (mReminderItems.size() < MAX_REMINDERS) {
1064             menu.setGroupVisible(MENU_GROUP_REMINDER, true);
1065             menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
1066         } else {
1067             menu.setGroupVisible(MENU_GROUP_REMINDER, false);
1068             menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
1069         }
1070
1071         if (mExtraOptions.getVisibility() == View.VISIBLE) {
1072             menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
1073             menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
1074         } else {
1075             menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
1076             menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
1077         }
1078
1079         return super.onPrepareOptionsMenu(menu);
1080     }
1081
1082     private void addReminder() {
1083         // TODO: when adding a new reminder, make it different from the
1084         // last one in the list (if any).
1085         if (mDefaultReminderMinutes == 0) {
1086             addReminder(this, this, mReminderItems, mReminderValues,
1087                     mReminderLabels, 10 /* minutes */);
1088         } else {
1089             addReminder(this, this, mReminderItems, mReminderValues,
1090                     mReminderLabels, mDefaultReminderMinutes);
1091         }
1092         updateRemindersVisibility();
1093     }
1094
1095     @Override
1096     public boolean onOptionsItemSelected(MenuItem item) {
1097         switch (item.getItemId()) {
1098         case MENU_ADD_REMINDER:
1099             addReminder();
1100             return true;
1101         case MENU_SHOW_EXTRA_OPTIONS:
1102             mExtraOptions.setVisibility(View.VISIBLE);
1103             return true;
1104         case MENU_HIDE_EXTRA_OPTIONS:
1105             mExtraOptions.setVisibility(View.GONE);
1106             return true;
1107         }
1108         return super.onOptionsItemSelected(item);
1109     }
1110
1111     @Override
1112     public void onBackPressed() {
1113         // If we are creating a new event, do not create it if the
1114         // title, location and description are all empty, in order to
1115         // prevent accidental "no subject" event creations.
1116         if (mUri != null || !isEmpty()) {
1117             if (!save()) {
1118                 // We cannot exit this activity because the calendars
1119                 // are still loading.
1120                 return;
1121             }
1122         }
1123         finish();
1124     }
1125
1126     private void populateWhen() {
1127         long startMillis = mStartTime.toMillis(false /* use isDst */);
1128         long endMillis = mEndTime.toMillis(false /* use isDst */);
1129         setDate(mStartDateButton, startMillis);
1130         setDate(mEndDateButton, endMillis);
1131
1132         setTime(mStartTimeButton, startMillis);
1133         setTime(mEndTimeButton, endMillis);
1134
1135         mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
1136         mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
1137
1138         mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
1139         mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
1140     }
1141
1142     private void populateRepeats() {
1143         Time time = mStartTime;
1144         Resources r = getResources();
1145         int resource = android.R.layout.simple_spinner_item;
1146
1147         String[] days = new String[] {
1148             DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
1149             DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
1150             DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
1151             DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
1152             DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
1153             DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
1154             DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
1155         };
1156         String[] ordinals = r.getStringArray(R.array.ordinal_labels);
1157
1158         // Only display "Custom" in the spinner if the device does not support the
1159         // recurrence functionality of the event. Only display every weekday if
1160         // the event starts on a weekday.
1161         boolean isCustomRecurrence = isCustomRecurrence();
1162         boolean isWeekdayEvent = isWeekdayEvent();
1163
1164         ArrayList<String> repeatArray = new ArrayList<String>(0);
1165         ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
1166
1167         repeatArray.add(r.getString(R.string.does_not_repeat));
1168         recurrenceIndexes.add(DOES_NOT_REPEAT);
1169
1170         repeatArray.add(r.getString(R.string.daily));
1171         recurrenceIndexes.add(REPEATS_DAILY);
1172
1173         if (isWeekdayEvent) {
1174             repeatArray.add(r.getString(R.string.every_weekday));
1175             recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
1176         }
1177
1178         String format = r.getString(R.string.weekly);
1179         repeatArray.add(String.format(format, time.format("%A")));
1180         recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
1181
1182         // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
1183         int dayNumber = (time.monthDay - 1) / 7;
1184         format = r.getString(R.string.monthly_on_day_count);
1185         repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
1186         recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
1187
1188         format = r.getString(R.string.monthly_on_day);
1189         repeatArray.add(String.format(format, time.monthDay));
1190         recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
1191
1192         long when = time.toMillis(false);
1193         format = r.getString(R.string.yearly);
1194         int flags = 0;
1195         if (DateFormat.is24HourFormat(this)) {
1196             flags |= DateUtils.FORMAT_24HOUR;
1197         }
1198         repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
1199         recurrenceIndexes.add(REPEATS_YEARLY);
1200
1201         if (isCustomRecurrence) {
1202             repeatArray.add(r.getString(R.string.custom));
1203             recurrenceIndexes.add(REPEATS_CUSTOM);
1204         }
1205         mRecurrenceIndexes = recurrenceIndexes;
1206
1207         int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
1208         if (mRrule != null) {
1209             if (isCustomRecurrence) {
1210                 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
1211             } else {
1212                 switch (mEventRecurrence.freq) {
1213                     case EventRecurrence.DAILY:
1214                         position = recurrenceIndexes.indexOf(REPEATS_DAILY);
1215                         break;
1216                     case EventRecurrence.WEEKLY:
1217                         if (mEventRecurrence.repeatsOnEveryWeekDay()) {
1218                             position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
1219                         } else {
1220                             position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
1221                         }
1222                         break;
1223                     case EventRecurrence.MONTHLY:
1224                         if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1225                             position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
1226                         } else {
1227                             position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
1228                         }
1229                         break;
1230                     case EventRecurrence.YEARLY:
1231                         position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
1232                         break;
1233                 }
1234             }
1235         }
1236         ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
1237         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1238         mRepeatsSpinner.setAdapter(adapter);
1239         mRepeatsSpinner.setSelection(position);
1240     }
1241
1242     // Adds a reminder to the displayed list of reminders.
1243     // Returns true if successfully added reminder, false if no reminders can
1244     // be added.
1245     static boolean addReminder(Activity activity, View.OnClickListener listener,
1246             ArrayList<LinearLayout> items, ArrayList<Integer> values,
1247             ArrayList<String> labels, int minutes) {
1248
1249         if (items.size() >= MAX_REMINDERS) {
1250             return false;
1251         }
1252
1253         LayoutInflater inflater = activity.getLayoutInflater();
1254         LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
1255         LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
1256         parent.addView(reminderItem);
1257
1258         Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
1259         Resources res = activity.getResources();
1260         spinner.setPrompt(res.getString(R.string.reminders_label));
1261         int resource = android.R.layout.simple_spinner_item;
1262         ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
1263         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1264         spinner.setAdapter(adapter);
1265
1266         ImageButton reminderRemoveButton;
1267         reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
1268         reminderRemoveButton.setOnClickListener(listener);
1269
1270         int index = findMinutesInReminderList(values, minutes);
1271         spinner.setSelection(index);
1272         items.add(reminderItem);
1273
1274         return true;
1275     }
1276
1277     static void addMinutesToList(Context context, ArrayList<Integer> values,
1278             ArrayList<String> labels, int minutes) {
1279         int index = values.indexOf(minutes);
1280         if (index != -1) {
1281             return;
1282         }
1283
1284         // The requested "minutes" does not exist in the list, so insert it
1285         // into the list.
1286
1287         String label = constructReminderLabel(context, minutes, false);
1288         int len = values.size();
1289         for (int i = 0; i < len; i++) {
1290             if (minutes < values.get(i)) {
1291                 values.add(i, minutes);
1292                 labels.add(i, label);
1293                 return;
1294             }
1295         }
1296
1297         values.add(minutes);
1298         labels.add(len, label);
1299     }
1300
1301     /**
1302      * Finds the index of the given "minutes" in the "values" list.
1303      *
1304      * @param values the list of minutes corresponding to the spinner choices
1305      * @param minutes the minutes to search for in the values list
1306      * @return the index of "minutes" in the "values" list
1307      */
1308     private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
1309         int index = values.indexOf(minutes);
1310         if (index == -1) {
1311             // This should never happen.
1312             Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
1313             return 0;
1314         }
1315         return index;
1316     }
1317
1318     // Constructs a label given an arbitrary number of minutes.  For example,
1319     // if the given minutes is 63, then this returns the string "63 minutes".
1320     // As another example, if the given minutes is 120, then this returns
1321     // "2 hours".
1322     static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
1323         Resources resources = context.getResources();
1324         int value, resId;
1325
1326         if (minutes % 60 != 0) {
1327             value = minutes;
1328             if (abbrev) {
1329                 resId = R.plurals.Nmins;
1330             } else {
1331                 resId = R.plurals.Nminutes;
1332             }
1333         } else if (minutes % (24 * 60) != 0) {
1334             value = minutes / 60;
1335             resId = R.plurals.Nhours;
1336         } else {
1337             value = minutes / ( 24 * 60);
1338             resId = R.plurals.Ndays;
1339         }
1340
1341         String format = resources.getQuantityString(resId, value);
1342         return String.format(format, value);
1343     }
1344
1345     private void updateRemindersVisibility() {
1346         if (mReminderItems.size() == 0) {
1347             mRemindersSeparator.setVisibility(View.GONE);
1348             mRemindersContainer.setVisibility(View.GONE);
1349         } else {
1350             mRemindersSeparator.setVisibility(View.VISIBLE);
1351             mRemindersContainer.setVisibility(View.VISIBLE);
1352         }
1353     }
1354
1355     private void setDate(TextView view, long millis) {
1356         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
1357                 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
1358                 DateUtils.FORMAT_ABBREV_WEEKDAY;
1359         view.setText(DateUtils.formatDateTime(this, millis, flags));
1360     }
1361
1362     private void setTime(TextView view, long millis) {
1363         int flags = DateUtils.FORMAT_SHOW_TIME;
1364         if (DateFormat.is24HourFormat(this)) {
1365             flags |= DateUtils.FORMAT_24HOUR;
1366         }
1367         view.setText(DateUtils.formatDateTime(this, millis, flags));
1368     }
1369
1370     // Saves the event.  Returns true if it is okay to exit this activity.
1371     private boolean save() {
1372         boolean forceSaveReminders = false;
1373
1374         // If we are creating a new event, then make sure we wait until the
1375         // query to fetch the list of calendars has finished.
1376         if (mEventCursor == null) {
1377             if (!mCalendarsQueryComplete) {
1378                 // Wait for the calendars query to finish.
1379                 if (mLoadingCalendarsDialog == null) {
1380                     // Create the progress dialog
1381                     mLoadingCalendarsDialog = ProgressDialog.show(this,
1382                             getText(R.string.loading_calendars_title),
1383                             getText(R.string.loading_calendars_message),
1384                             true, true, this);
1385                     mSaveAfterQueryComplete = true;
1386                 }
1387                 return false;
1388             }
1389
1390             // Avoid creating a new event if the calendars cursor is empty or we clicked through
1391             // too quickly and no calendar was selected (blame the monkey)
1392             if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0 ||
1393                     mCalendarsSpinner.getSelectedItemId() == AdapterView.INVALID_ROW_ID) {
1394                 Log.w("Cal", "The calendars table does not contain any calendars"
1395                         + " or no calendar was selected."
1396                         + " New event was not created.");
1397                 return true;
1398             }
1399             Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
1400         } else {
1401             Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
1402         }
1403
1404         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1405         int eventIdIndex = -1;
1406
1407         ContentValues values = getContentValuesFromUi();
1408         Uri uri = mUri;
1409
1410         // Update the "hasAlarm" field for the event
1411         ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
1412                 mReminderValues);
1413         int len = reminderMinutes.size();
1414         values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
1415
1416         // For recurring events, we must make sure that we use duration rather
1417         // than dtend.
1418         if (uri == null) {
1419             // Add hasAttendeeData for a new event
1420             values.put(Events.HAS_ATTENDEE_DATA, 1);
1421             // Create new event with new contents
1422             addRecurrenceRule(values);
1423             eventIdIndex = ops.size();
1424             Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1425             ops.add(b.build());
1426             forceSaveReminders = true;
1427
1428         } else if (mRrule == null) {
1429             // Modify contents of a non-repeating event
1430             addRecurrenceRule(values);
1431             checkTimeDependentFields(values);
1432             ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1433
1434         } else if (mInitialValues.getAsString(Events.RRULE) == null) {
1435             // This event was changed from a non-repeating event to a
1436             // repeating event.
1437             addRecurrenceRule(values);
1438             values.remove(Events.DTEND);
1439             ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1440
1441         } else if (mModification == MODIFY_SELECTED) {
1442             // Modify contents of the current instance of repeating event
1443
1444             // Create a recurrence exception
1445             long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1446             values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
1447             values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
1448             boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1449             values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
1450
1451             eventIdIndex = ops.size();
1452             Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1453             ops.add(b.build());
1454             forceSaveReminders = true;
1455
1456         } else if (mModification == MODIFY_ALL_FOLLOWING) {
1457             // Modify this instance and all future instances of repeating event
1458             addRecurrenceRule(values);
1459
1460             if (mRrule == null) {
1461                 // We've changed a recurring event to a non-recurring event.
1462                 // If the event we are editing is the first in the series,
1463                 // then delete the whole series.  Otherwise, update the series
1464                 // to end at the new start time.
1465                 if (isFirstEventInSeries()) {
1466                     ops.add(ContentProviderOperation.newDelete(uri).build());
1467                 } else {
1468                     // Update the current repeating event to end at the new
1469                     // start time.
1470                     updatePastEvents(ops, uri);
1471                 }
1472                 eventIdIndex = ops.size();
1473                 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1474                         .build());
1475             } else {
1476                 if (isFirstEventInSeries()) {
1477                     checkTimeDependentFields(values);
1478                     values.remove(Events.DTEND);
1479                     Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
1480                     ops.add(b.build());
1481                 } else {
1482                     // Update the current repeating event to end at the new
1483                     // start time.
1484                     updatePastEvents(ops, uri);
1485
1486                     // Create a new event with the user-modified fields
1487                     values.remove(Events.DTEND);
1488                     eventIdIndex = ops.size();
1489                     ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
1490                             values).build());
1491                 }
1492             }
1493             forceSaveReminders = true;
1494
1495         } else if (mModification == MODIFY_ALL) {
1496
1497             // Modify all instances of repeating event
1498             addRecurrenceRule(values);
1499
1500             if (mRrule == null) {
1501                 // We've changed a recurring event to a non-recurring event.
1502                 // Delete the whole series and replace it with a new
1503                 // non-recurring event.
1504                 ops.add(ContentProviderOperation.newDelete(uri).build());
1505
1506                 eventIdIndex = ops.size();
1507                 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1508                         .build());
1509                 forceSaveReminders = true;
1510             } else {
1511                 checkTimeDependentFields(values);
1512                 values.remove(Events.DTEND);
1513                 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1514             }
1515         }
1516
1517         // New Event or New Exception to an existing event
1518         boolean newEvent = (eventIdIndex != -1);
1519
1520         if (newEvent) {
1521             saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
1522                     forceSaveReminders);
1523         } else if (uri != null) {
1524             long eventId = ContentUris.parseId(uri);
1525             saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
1526                     forceSaveReminders);
1527         }
1528
1529         Builder b;
1530
1531         // New event/instance - Set Organizer's response as yes
1532         if (mHasAttendeeData && newEvent) {
1533             values.clear();
1534             int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
1535
1536             // Save the default calendar for new events
1537             if (mCalendarsCursor != null) {
1538                 if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
1539                     String defaultCalendar = mCalendarsCursor
1540                             .getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1541                     Utils.setSharedPreference(this,
1542                             CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, defaultCalendar);
1543                 }
1544             }
1545
1546             String ownerEmail = mOwnerAccount;
1547             // Just in case mOwnerAccount is null, try to get owner from mCalendarsCursor
1548             if (ownerEmail == null && mCalendarsCursor != null &&
1549                     mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
1550                 ownerEmail = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1551             }
1552             if (ownerEmail != null) {
1553                 values.put(Attendees.ATTENDEE_EMAIL, ownerEmail);
1554                 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
1555                 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1556                 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
1557
1558                 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1559                         .withValues(values);
1560                 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1561                 ops.add(b.build());
1562             }
1563         }
1564
1565         // TODO: is this the right test?  this currently checks if this is
1566         // a new event or an existing event.  or is this a paranoia check?
1567         if (mHasAttendeeData && (newEvent || uri != null)) {
1568             Editable attendeesText = mAttendeesList.getText();
1569             // Hit the content provider only if this is a new event or the user has changed it
1570             if (newEvent || !mOriginalAttendees.equals(attendeesText.toString())) {
1571                 // figure out which attendees need to be added and which ones
1572                 // need to be deleted.  use a linked hash set, so we maintain
1573                 // order (but also remove duplicates).
1574                 LinkedHashSet<Rfc822Token> newAttendees = getAddressesFromList(mAttendeesList);
1575
1576                 // the eventId is only used if eventIdIndex is -1.
1577                 // TODO: clean up this code.
1578                 long eventId = uri != null ? ContentUris.parseId(uri) : -1;
1579
1580                 // only compute deltas if this is an existing event.
1581                 // new events (being inserted into the Events table) won't
1582                 // have any existing attendees.
1583                 if (!newEvent) {
1584                     HashSet<Rfc822Token> removedAttendees = new HashSet<Rfc822Token>();
1585                     HashSet<Rfc822Token> originalAttendees = new HashSet<Rfc822Token>();
1586                     Rfc822Tokenizer.tokenize(mOriginalAttendees, originalAttendees);
1587                     for (Rfc822Token originalAttendee : originalAttendees) {
1588                         if (newAttendees.contains(originalAttendee)) {
1589                             // existing attendee.  remove from new attendees set.
1590                             newAttendees.remove(originalAttendee);
1591                         } else {
1592                             // no longer in attendees.  mark as removed.
1593                             removedAttendees.add(originalAttendee);
1594                         }
1595                     }
1596
1597                     // delete removed attendees
1598                     b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
1599
1600                     String[] args = new String[removedAttendees.size() + 1];
1601                     args[0] = Long.toString(eventId);
1602                     int i = 1;
1603                     StringBuilder deleteWhere = new StringBuilder(ATTENDEES_DELETE_PREFIX);
1604                     for (Rfc822Token removedAttendee : removedAttendees) {
1605                         if (i > 1) {
1606                             deleteWhere.append(",");
1607                         }
1608                         deleteWhere.append("?");
1609                         args[i++] = removedAttendee.getAddress();
1610                     }
1611                     deleteWhere.append(")");
1612                     b.withSelection(deleteWhere.toString(), args);
1613                     ops.add(b.build());
1614                 }
1615
1616                 if (newAttendees.size() > 0) {
1617                     // Insert the new attendees
1618                     for (Rfc822Token attendee : newAttendees) {
1619                         values.clear();
1620                         values.put(Attendees.ATTENDEE_NAME, attendee.getName());
1621                         values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
1622                         values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
1623                         values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1624                         values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
1625
1626                         if (newEvent) {
1627                             b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1628                                     .withValues(values);
1629                             b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
1630                         } else {
1631                             values.put(Attendees.EVENT_ID, eventId);
1632                             b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1633                                     .withValues(values);
1634                         }
1635                         ops.add(b.build());
1636                     }
1637                 }
1638             }
1639         }
1640
1641         try {
1642             // TODO Move this to background thread
1643             ContentProviderResult[] results =
1644                 getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
1645             if (DEBUG) {
1646                 for (int i = 0; i < results.length; i++) {
1647                     Log.v(TAG, "results = " + results[i].toString());
1648                 }
1649             }
1650         } catch (RemoteException e) {
1651             Log.w(TAG, "Ignoring unexpected remote exception", e);
1652         } catch (OperationApplicationException e) {
1653             Log.w(TAG, "Ignoring unexpected exception", e);
1654         }
1655
1656         return true;
1657     }
1658
1659     private boolean isFirstEventInSeries() {
1660         int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
1661         long start = mEventCursor.getLong(dtStart);
1662         return start == mStartTime.toMillis(true);
1663     }
1664
1665     private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) {
1666         long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1667         String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
1668         boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1669         String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
1670         mEventRecurrence.parse(oldRrule);
1671
1672         Time untilTime = new Time();
1673         long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1674         ContentValues oldValues = new ContentValues();
1675
1676         // The "until" time must be in UTC time in order for Google calendar
1677         // to display it properly.  For all-day events, the "until" time string
1678         // must include just the date field, and not the time field.  The
1679         // repeating events repeat up to and including the "until" time.
1680         untilTime.timezone = Time.TIMEZONE_UTC;
1681
1682         // Subtract one second from the old begin time to get the new
1683         // "until" time.
1684         untilTime.set(begin - 1000);  // subtract one second (1000 millis)
1685         if (allDay) {
1686             untilTime.hour = 0;
1687             untilTime.minute = 0;
1688             untilTime.second = 0;
1689             untilTime.allDay = true;
1690             untilTime.normalize(false);
1691
1692             // For all-day events, the duration must be in days, not seconds.
1693             // Otherwise, Google Calendar will (mistakenly) change this event
1694             // into a non-all-day event.
1695             int len = oldDuration.length();
1696             if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
1697                 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
1698                 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1699                 oldDuration = "P" + days + "D";
1700             }
1701         }
1702         mEventRecurrence.until = untilTime.format2445();
1703
1704         oldValues.put(Events.DTSTART, oldStartMillis);
1705         oldValues.put(Events.DURATION, oldDuration);
1706         oldValues.put(Events.RRULE, mEventRecurrence.toString());
1707         Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
1708         ops.add(b.build());
1709     }
1710
1711     private void checkTimeDependentFields(ContentValues values) {
1712         long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1713         long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
1714         boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1715         String oldRrule = mInitialValues.getAsString(Events.RRULE);
1716         String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
1717
1718         long newBegin = values.getAsLong(Events.DTSTART);
1719         long newEnd = values.getAsLong(Events.DTEND);
1720         boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
1721         String newRrule = values.getAsString(Events.RRULE);
1722         String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1723
1724         // If none of the time-dependent fields changed, then remove them.
1725         if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
1726                 && TextUtils.equals(oldRrule, newRrule)
1727                 && TextUtils.equals(oldTimezone, newTimezone)) {
1728             values.remove(Events.DTSTART);
1729             values.remove(Events.DTEND);
1730             values.remove(Events.DURATION);
1731             values.remove(Events.ALL_DAY);
1732             values.remove(Events.RRULE);
1733             values.remove(Events.EVENT_TIMEZONE);
1734             return;
1735         }
1736
1737         if (oldRrule == null || newRrule == null) {
1738             return;
1739         }
1740
1741         // If we are modifying all events then we need to set DTSTART to the
1742         // start time of the first event in the series, not the current
1743         // date and time.  If the start time of the event was changed
1744         // (from, say, 3pm to 4pm), then we want to add the time difference
1745         // to the start time of the first event in the series (the DTSTART
1746         // value).  If we are modifying one instance or all following instances,
1747         // then we leave the DTSTART field alone.
1748         if (mModification == MODIFY_ALL) {
1749             long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1750             if (oldBegin != newBegin) {
1751                 // The user changed the start time of this event
1752                 long offset = newBegin - oldBegin;
1753                 oldStartMillis += offset;
1754             }
1755             values.put(Events.DTSTART, oldStartMillis);
1756         }
1757     }
1758
1759     static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
1760             ArrayList<Integer> reminderValues) {
1761         int len = reminderItems.size();
1762         ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
1763         for (int index = 0; index < len; index++) {
1764             LinearLayout layout = reminderItems.get(index);
1765             Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
1766             int minutes = reminderValues.get(spinner.getSelectedItemPosition());
1767             reminderMinutes.add(minutes);
1768         }
1769         return reminderMinutes;
1770     }
1771
1772     /**
1773      * Saves the reminders, if they changed.  Returns true if the database
1774      * was updated.
1775      *
1776      * @param ops the array of ContentProviderOperations
1777      * @param eventId the id of the event whose reminders are being updated
1778      * @param reminderMinutes the array of reminders set by the user
1779      * @param originalMinutes the original array of reminders
1780      * @param forceSave if true, then save the reminders even if they didn't
1781      *   change
1782      * @return true if the database was updated
1783      */
1784     static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
1785             ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
1786             boolean forceSave) {
1787         // If the reminders have not changed, then don't update the database
1788         if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1789             return false;
1790         }
1791
1792         // Delete all the existing reminders for this event
1793         String where = Reminders.EVENT_ID + "=?";
1794         String[] args = new String[] { Long.toString(eventId) };
1795         Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1796         b.withSelection(where, args);
1797         ops.add(b.build());
1798
1799         ContentValues values = new ContentValues();
1800         int len = reminderMinutes.size();
1801
1802         // Insert the new reminders, if any
1803         for (int i = 0; i < len; i++) {
1804             int minutes = reminderMinutes.get(i);
1805
1806             values.clear();
1807             values.put(Reminders.MINUTES, minutes);
1808             values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1809             values.put(Reminders.EVENT_ID, eventId);
1810             b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1811             ops.add(b.build());
1812         }
1813         return true;
1814     }
1815
1816     static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
1817             int eventIdIndex, ArrayList<Integer> reminderMinutes,
1818             ArrayList<Integer> originalMinutes, boolean forceSave) {
1819         // If the reminders have not changed, then don't update the database
1820         if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1821             return false;
1822         }
1823
1824         // Delete all the existing reminders for this event
1825         Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1826         b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
1827         b.withSelectionBackReference(0, eventIdIndex);
1828         ops.add(b.build());
1829
1830         ContentValues values = new ContentValues();
1831         int len = reminderMinutes.size();
1832
1833         // Insert the new reminders, if any
1834         for (int i = 0; i < len; i++) {
1835             int minutes = reminderMinutes.get(i);
1836
1837             values.clear();
1838             values.put(Reminders.MINUTES, minutes);
1839             values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1840             b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1841             b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1842             ops.add(b.build());
1843         }
1844         return true;
1845     }
1846
1847     private void addRecurrenceRule(ContentValues values) {
1848         updateRecurrenceRule();
1849
1850         if (mRrule == null) {
1851             return;
1852         }
1853
1854         values.put(Events.RRULE, mRrule);
1855         long end = mEndTime.toMillis(true /* ignore dst */);
1856         long start = mStartTime.toMillis(true /* ignore dst */);
1857         String duration;
1858
1859         boolean isAllDay = mAllDayCheckBox.isChecked();
1860         if (isAllDay) {
1861             long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
1862             duration = "P" + days + "D";
1863         } else {
1864             long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
1865             duration = "P" + seconds + "S";
1866         }
1867         values.put(Events.DURATION, duration);
1868     }
1869
1870     private void updateRecurrenceRule() {
1871         int position = mRepeatsSpinner.getSelectedItemPosition();
1872         int selection = mRecurrenceIndexes.get(position);
1873
1874         if (selection == DOES_NOT_REPEAT) {
1875             mRrule = null;
1876             return;
1877         } else if (selection == REPEATS_CUSTOM) {
1878             // Keep custom recurrence as before.
1879             return;
1880         } else if (selection == REPEATS_DAILY) {
1881             mEventRecurrence.freq = EventRecurrence.DAILY;
1882         } else if (selection == REPEATS_EVERY_WEEKDAY) {
1883             mEventRecurrence.freq = EventRecurrence.WEEKLY;
1884             int dayCount = 5;
1885             int[] byday = new int[dayCount];
1886             int[] bydayNum = new int[dayCount];
1887
1888             byday[0] = EventRecurrence.MO;
1889             byday[1] = EventRecurrence.TU;
1890             byday[2] = EventRecurrence.WE;
1891             byday[3] = EventRecurrence.TH;
1892             byday[4] = EventRecurrence.FR;
1893             for (int day = 0; day < dayCount; day++) {
1894                 bydayNum[day] = 0;
1895             }
1896
1897             mEventRecurrence.byday = byday;
1898             mEventRecurrence.bydayNum = bydayNum;
1899             mEventRecurrence.bydayCount = dayCount;
1900         } else if (selection == REPEATS_WEEKLY_ON_DAY) {
1901             mEventRecurrence.freq = EventRecurrence.WEEKLY;
1902             int[] days = new int[1];
1903             int dayCount = 1;
1904             int[] dayNum = new int[dayCount];
1905
1906             days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1907             // not sure why this needs to be zero, but set it for now.
1908             dayNum[0] = 0;
1909
1910             mEventRecurrence.byday = days;
1911             mEventRecurrence.bydayNum = dayNum;
1912             mEventRecurrence.bydayCount = dayCount;
1913         } else if (selection == REPEATS_MONTHLY_ON_DAY) {
1914             mEventRecurrence.freq = EventRecurrence.MONTHLY;
1915             mEventRecurrence.bydayCount = 0;
1916             mEventRecurrence.bymonthdayCount = 1;
1917             int[] bymonthday = new int[1];
1918             bymonthday[0] = mStartTime.monthDay;
1919             mEventRecurrence.bymonthday = bymonthday;
1920         } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
1921             mEventRecurrence.freq = EventRecurrence.MONTHLY;
1922             mEventRecurrence.bydayCount = 1;
1923             mEventRecurrence.bymonthdayCount = 0;
1924
1925             int[] byday = new int[1];
1926             int[] bydayNum = new int[1];
1927             // Compute the week number (for example, the "2nd" Monday)
1928             int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
1929             if (dayCount == 5) {
1930                 dayCount = -1;
1931             }
1932             bydayNum[0] = dayCount;
1933             byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1934             mEventRecurrence.byday = byday;
1935             mEventRecurrence.bydayNum = bydayNum;
1936         } else if (selection == REPEATS_YEARLY) {
1937             mEventRecurrence.freq = EventRecurrence.YEARLY;
1938         }
1939
1940         // Set the week start day.
1941         mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
1942         mRrule = mEventRecurrence.toString();
1943     }
1944
1945     private ContentValues getContentValuesFromUi() {
1946         String title = mTitleTextView.getText().toString().trim();
1947         boolean isAllDay = mAllDayCheckBox.isChecked();
1948         String location = mLocationTextView.getText().toString().trim();
1949         String description = mDescriptionTextView.getText().toString().trim();
1950
1951         ContentValues values = new ContentValues();
1952
1953         String timezone = null;
1954         long startMillis;
1955         long endMillis;
1956         long calendarId;
1957         if (isAllDay) {
1958             // Reset start and end time, increment the monthDay by 1, and set
1959             // the timezone to UTC, as required for all-day events.
1960             timezone = Time.TIMEZONE_UTC;
1961             mStartTime.hour = 0;
1962             mStartTime.minute = 0;
1963             mStartTime.second = 0;
1964             mStartTime.timezone = timezone;
1965             startMillis = mStartTime.normalize(true);
1966
1967             mEndTime.hour = 0;
1968             mEndTime.minute = 0;
1969             mEndTime.second = 0;
1970             mEndTime.monthDay++;
1971             mEndTime.timezone = timezone;
1972             endMillis = mEndTime.normalize(true);
1973
1974             if (mEventCursor == null) {
1975                 // This is a new event
1976                 calendarId = mCalendarsSpinner.getSelectedItemId();
1977             } else {
1978                 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1979             }
1980         } else {
1981             startMillis = mStartTime.toMillis(true);
1982             endMillis = mEndTime.toMillis(true);
1983             if (mEventCursor != null) {
1984                 // This is an existing event
1985                 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
1986
1987                 // The timezone might be null if we are changing an existing
1988                 // all-day event to a non-all-day event.  We need to assign
1989                 // a timezone to the non-all-day event.
1990                 if (TextUtils.isEmpty(timezone)) {
1991                     timezone = TimeZone.getDefault().getID();
1992                 }
1993                 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1994             } else {
1995                 // This is a new event
1996                 calendarId = mCalendarsSpinner.getSelectedItemId();
1997
1998                 // The timezone for a new event is the currently displayed
1999                 // timezone, NOT the timezone of the containing calendar.
2000                 timezone = TimeZone.getDefault().getID();
2001             }
2002         }
2003
2004         values.put(Events.CALENDAR_ID, calendarId);
2005         values.put(Events.EVENT_TIMEZONE, timezone);
2006         values.put(Events.TITLE, title);
2007         values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
2008         values.put(Events.DTSTART, startMillis);
2009         values.put(Events.DTEND, endMillis);
2010         values.put(Events.DESCRIPTION, description);
2011         values.put(Events.EVENT_LOCATION, location);
2012         values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
2013
2014         int visibility = mVisibilitySpinner.getSelectedItemPosition();
2015         if (visibility > 0) {
2016             // For now we the array contains the values 0, 2, and 3. We add one to match.
2017             visibility++;
2018         }
2019         values.put(Events.VISIBILITY, visibility);
2020
2021         return values;
2022     }
2023
2024     private boolean isEmpty() {
2025         String title = mTitleTextView.getText().toString().trim();
2026         if (title.length() > 0) {
2027             return false;
2028         }
2029
2030         String location = mLocationTextView.getText().toString().trim();
2031         if (location.length() > 0) {
2032             return false;
2033         }
2034
2035         String description = mDescriptionTextView.getText().toString().trim();
2036         if (description.length() > 0) {
2037             return false;
2038         }
2039
2040         return true;
2041     }
2042
2043     private boolean isCustomRecurrence() {
2044
2045         if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
2046             return true;
2047         }
2048
2049         if (mEventRecurrence.freq == 0) {
2050             return false;
2051         }
2052
2053         switch (mEventRecurrence.freq) {
2054         case EventRecurrence.DAILY:
2055             return false;
2056         case EventRecurrence.WEEKLY:
2057             if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
2058                 return false;
2059             } else if (mEventRecurrence.bydayCount == 1) {
2060                 return false;
2061             }
2062             break;
2063         case EventRecurrence.MONTHLY:
2064             if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
2065                 return false;
2066             } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
2067                 return false;
2068             }
2069             break;
2070         case EventRecurrence.YEARLY:
2071             return false;
2072         }
2073
2074         return true;
2075     }
2076
2077     private boolean isWeekdayEvent() {
2078         if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {
2079             return true;
2080         }
2081         return false;
2082     }
2083 }