• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TimeListener(View view)288         public TimeListener(View view) {
289             mView = view;
290         }
291 
onTimeSet(TimePicker view, int hourOfDay, int minute)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 
TimeClickListener(Time time)338         public TimeClickListener(Time time) {
339             mTime = time;
340         }
341 
onClick(View v)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 
DateListener(View view)352         public DateListener(View view) {
353             mView = view;
354         }
355 
onDateSet(DatePicker view, int year, int month, int monthDay)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 
DateClickListener(Time time)410         public DateClickListener(Time time) {
411             mTime = time;
412         }
413 
onClick(View v)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 {
CalendarsAdapter(Context context, Cursor c)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
bindView(View view, Context context, Cursor cursor)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.
onClick(View v)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.
onCancel(DialogInterface dialog)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.
onClick(DialogInterface dialog, int which)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 {
QueryHandler(ContentResolver cr)522         public QueryHandler(ContentResolver cr) {
523             super(cr);
524         }
525 
526         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)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
findDefaultCalendarPosition(Cursor calendarsCursor)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 
extractDomain(String email)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
onCreate(Bundle icicle)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 = getResources().getString(R.string.google_email_domain);
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 
getAddressesFromList(MultiAutoCompleteTextView list)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
initMultiAutoCompleteTextView(int res)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 
initFromIntent(Intent intent)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 (!TextUtils.isEmpty(rrule)) {
990             mRrule = rrule;
991             mEventRecurrence.parse(rrule);
992         }
993     }
994 
995     @Override
onResume()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
onCreateOptionsMenu(Menu menu)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
onPrepareOptionsMenu(Menu menu)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 
addReminder()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
onOptionsItemSelected(MenuItem item)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
onBackPressed()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 
populateWhen()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 
populateTimezone()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      */
updateHomeTime()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      */
showAllTimezone(ListView listView)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 
showTimezoneDialog()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 
populateRepeats()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 (!TextUtils.isEmpty(mRrule)) {
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.
addReminder(Activity activity, View.OnClickListener listener, ArrayList<LinearLayout> items, ArrayList<Integer> values, ArrayList<String> labels, int minutes)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 
addMinutesToList(Context context, ArrayList<Integer> values, ArrayList<String> labels, int minutes)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      */
findMinutesInReminderList(ArrayList<Integer> values, int minutes)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".
constructReminderLabel(Context context, int minutes, boolean abbrev)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 
updateRemindersVisibility()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 
setDate(TextView view, long millis)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 
setTime(TextView view, long millis)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 
setTimezone(int i)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.
save()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 (!TextUtils.isEmpty(mRrule)) {
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 (TextUtils.isEmpty(mRrule)) {
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 (TextUtils.isEmpty(mInitialValues.getAsString(Events.RRULE))) {
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 (TextUtils.isEmpty(mRrule)) {
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 (TextUtils.isEmpty(mRrule)) {
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 
isFirstEventInSeries()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 
updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri)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 
checkTimeDependentFields(ContentValues values)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 (TextUtils.isEmpty(oldRrule) || TextUtils.isEmpty(newRrule)) {
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 
reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems, ArrayList<Integer> reminderValues)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      */
saveReminders(ArrayList<ContentProviderOperation> ops, long eventId, ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes, boolean forceSave)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 
saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops, int eventIdIndex, ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes, boolean forceSave)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 
addRecurrenceRule(ContentValues values)2098     private void addRecurrenceRule(ContentValues values) {
2099         updateRecurrenceRule();
2100 
2101         if (TextUtils.isEmpty(mRrule)) {
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 
clearRecurrence()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 
updateRecurrenceRule()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 
getContentValuesFromUi()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 
isEmpty()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 
isCustomRecurrence()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 
isWeekdayEvent()2329     private boolean isWeekdayEvent() {
2330         if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {
2331             return true;
2332         }
2333         return false;
2334     }
2335 }
2336