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