OSDN Git Service

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