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