OSDN Git Service

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