OSDN Git Service

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