OSDN Git Service

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