OSDN Git Service

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