OSDN Git Service

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