OSDN Git Service

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