OSDN Git Service

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