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