2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.calendar;
19 import static android.provider.Calendar.EVENT_BEGIN_TIME;
20 import static android.provider.Calendar.EVENT_END_TIME;
22 import com.android.common.Rfc822InputFilter;
23 import com.android.common.Rfc822Validator;
25 import android.app.Activity;
26 import android.app.AlertDialog;
27 import android.app.DatePickerDialog;
28 import android.app.ProgressDialog;
29 import android.app.TimePickerDialog;
30 import android.app.DatePickerDialog.OnDateSetListener;
31 import android.app.TimePickerDialog.OnTimeSetListener;
32 import android.content.AsyncQueryHandler;
33 import android.content.ContentProviderOperation;
34 import android.content.ContentProviderResult;
35 import android.content.ContentResolver;
36 import android.content.ContentUris;
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.DialogInterface;
40 import android.content.Intent;
41 import android.content.OperationApplicationException;
42 import android.content.SharedPreferences;
43 import android.content.ContentProviderOperation.Builder;
44 import android.content.DialogInterface.OnCancelListener;
45 import android.content.DialogInterface.OnClickListener;
46 import android.content.res.Resources;
47 import android.database.Cursor;
48 import android.net.Uri;
49 import android.os.Bundle;
50 import android.os.RemoteException;
51 import android.pim.EventRecurrence;
52 import android.provider.Calendar.Attendees;
53 import android.provider.Calendar.Calendars;
54 import android.provider.Calendar.Events;
55 import android.provider.Calendar.Reminders;
56 import android.text.Editable;
57 import android.text.InputFilter;
58 import android.text.TextUtils;
59 import android.text.format.DateFormat;
60 import android.text.format.DateUtils;
61 import android.text.format.Time;
62 import android.text.util.Rfc822Token;
63 import android.text.util.Rfc822Tokenizer;
64 import android.util.Log;
65 import android.view.LayoutInflater;
66 import android.view.Menu;
67 import android.view.MenuItem;
68 import android.view.View;
69 import android.view.Window;
70 import android.widget.AdapterView;
71 import android.widget.ArrayAdapter;
72 import android.widget.Button;
73 import android.widget.CheckBox;
74 import android.widget.CompoundButton;
75 import android.widget.DatePicker;
76 import android.widget.ImageButton;
77 import android.widget.LinearLayout;
78 import android.widget.MultiAutoCompleteTextView;
79 import android.widget.ResourceCursorAdapter;
80 import android.widget.Spinner;
81 import android.widget.TextView;
82 import android.widget.TimePicker;
83 import android.widget.Toast;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.Calendar;
88 import java.util.HashMap;
89 import java.util.HashSet;
90 import java.util.Iterator;
91 import java.util.LinkedHashSet;
92 import java.util.TimeZone;
94 public class EditEvent extends Activity implements View.OnClickListener,
95 DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
96 private static final String TAG = "EditEvent";
97 private static final boolean DEBUG = false;
100 * This is the symbolic name for the key used to pass in the boolean
101 * for creating all-day events that is part of the extra data of the intent.
102 * This is used only for creating new events and is set to true if
103 * the default for the new event should be an all-day event.
105 public static final String EVENT_ALL_DAY = "allDay";
107 private static final int MAX_REMINDERS = 5;
109 private static final int MENU_GROUP_REMINDER = 1;
110 private static final int MENU_GROUP_SHOW_OPTIONS = 2;
111 private static final int MENU_GROUP_HIDE_OPTIONS = 3;
113 private static final int MENU_ADD_REMINDER = 1;
114 private static final int MENU_SHOW_EXTRA_OPTIONS = 2;
115 private static final int MENU_HIDE_EXTRA_OPTIONS = 3;
117 private static final String[] EVENT_PROJECTION = new String[] {
120 Events.DESCRIPTION, // 2
121 Events.EVENT_LOCATION, // 3
123 Events.HAS_ALARM, // 5
124 Events.CALENDAR_ID, // 6
126 Events.DURATION, // 8
127 Events.EVENT_TIMEZONE, // 9
129 Events._SYNC_ID, // 11
130 Events.TRANSPARENCY, // 12
131 Events.VISIBILITY, // 13
132 Events.OWNER_ACCOUNT, // 14
133 Events.HAS_ATTENDEE_DATA, // 15
135 private static final int EVENT_INDEX_ID = 0;
136 private static final int EVENT_INDEX_TITLE = 1;
137 private static final int EVENT_INDEX_DESCRIPTION = 2;
138 private static final int EVENT_INDEX_EVENT_LOCATION = 3;
139 private static final int EVENT_INDEX_ALL_DAY = 4;
140 private static final int EVENT_INDEX_HAS_ALARM = 5;
141 private static final int EVENT_INDEX_CALENDAR_ID = 6;
142 private static final int EVENT_INDEX_DTSTART = 7;
143 private static final int EVENT_INDEX_DURATION = 8;
144 private static final int EVENT_INDEX_TIMEZONE = 9;
145 private static final int EVENT_INDEX_RRULE = 10;
146 private static final int EVENT_INDEX_SYNC_ID = 11;
147 private static final int EVENT_INDEX_TRANSPARENCY = 12;
148 private static final int EVENT_INDEX_VISIBILITY = 13;
149 private static final int EVENT_INDEX_OWNER_ACCOUNT = 14;
150 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 15;
152 private static final String[] CALENDARS_PROJECTION = new String[] {
154 Calendars.DISPLAY_NAME, // 1
155 Calendars.OWNER_ACCOUNT, // 2
156 Calendars.COLOR, // 3
158 private static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
159 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
160 private static final int CALENDARS_INDEX_COLOR = 3;
161 private static final String CALENDARS_WHERE = Calendars.ACCESS_LEVEL + ">=" +
162 Calendars.CONTRIBUTOR_ACCESS + " AND " + Calendars.SYNC_EVENTS + "=1";
164 private static final String[] REMINDERS_PROJECTION = new String[] {
166 Reminders.MINUTES, // 1
168 private static final int REMINDERS_INDEX_MINUTES = 1;
169 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
170 Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
171 Reminders.METHOD_DEFAULT + ")";
173 private static final String[] ATTENDEES_PROJECTION = new String[] {
174 Attendees.ATTENDEE_NAME, // 0
175 Attendees.ATTENDEE_EMAIL, // 1
177 private static final int ATTENDEES_INDEX_NAME = 0;
178 private static final int ATTENDEES_INDEX_EMAIL = 1;
179 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=? AND "
180 + Attendees.ATTENDEE_RELATIONSHIP + "<>" + Attendees.RELATIONSHIP_ORGANIZER;
181 private static final String ATTENDEES_DELETE_PREFIX = Attendees.EVENT_ID + "=? AND " +
182 Attendees.ATTENDEE_EMAIL + " IN (";
184 private static final int DOES_NOT_REPEAT = 0;
185 private static final int REPEATS_DAILY = 1;
186 private static final int REPEATS_EVERY_WEEKDAY = 2;
187 private static final int REPEATS_WEEKLY_ON_DAY = 3;
188 private static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
189 private static final int REPEATS_MONTHLY_ON_DAY = 5;
190 private static final int REPEATS_YEARLY = 6;
191 private static final int REPEATS_CUSTOM = 7;
193 private static final int MODIFY_UNINITIALIZED = 0;
194 private static final int MODIFY_SELECTED = 1;
195 private static final int MODIFY_ALL = 2;
196 private static final int MODIFY_ALL_FOLLOWING = 3;
198 private static final int DAY_IN_SECONDS = 24 * 60 * 60;
200 private int mFirstDayOfWeek; // cached in onCreate
202 private Cursor mEventCursor;
203 private Cursor mCalendarsCursor;
205 private Button mStartDateButton;
206 private Button mEndDateButton;
207 private Button mStartTimeButton;
208 private Button mEndTimeButton;
209 private Button mSaveButton;
210 private Button mDeleteButton;
211 private Button mDiscardButton;
212 private CheckBox mAllDayCheckBox;
213 private Spinner mCalendarsSpinner;
214 private Spinner mRepeatsSpinner;
215 private Spinner mAvailabilitySpinner;
216 private Spinner mVisibilitySpinner;
217 private TextView mTitleTextView;
218 private TextView mLocationTextView;
219 private TextView mDescriptionTextView;
220 private View mRemindersSeparator;
221 private LinearLayout mRemindersContainer;
222 private LinearLayout mExtraOptions;
223 private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
224 private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
225 private Rfc822Validator mEmailValidator;
226 private MultiAutoCompleteTextView mAttendeesList;
227 private EmailAddressAdapter mAddressAdapter;
228 private String mOriginalAttendees = "";
230 // Used to control the visibility of the Guests textview. Default to true
231 private boolean mHasAttendeeData = true;
233 private EventRecurrence mEventRecurrence = new EventRecurrence();
234 private String mRrule;
235 private boolean mCalendarsQueryComplete;
236 private boolean mSaveAfterQueryComplete;
237 private ProgressDialog mLoadingCalendarsDialog;
238 private AlertDialog mNoCalendarsDialog;
239 private ContentValues mInitialValues;
240 private String mOwnerAccount;
243 * If the repeating event is created on the phone and it hasn't been
244 * synced yet to the web server, then there is a bug where you can't
245 * delete or change an instance of the repeating event. This case
246 * can be detected with mSyncId. If mSyncId == null, then the repeating
247 * event has not been synced to the phone, in which case we won't allow
248 * the user to change one instance.
250 private String mSyncId;
252 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0);
253 private ArrayList<Integer> mReminderValues;
254 private ArrayList<String> mReminderLabels;
256 private Time mStartTime;
257 private Time mEndTime;
258 private int mModification = MODIFY_UNINITIALIZED;
259 private int mDefaultReminderMinutes;
261 private DeleteEventHelper mDeleteEventHelper;
262 private QueryHandler mQueryHandler;
264 /* This class is used to update the time buttons. */
265 private class TimeListener implements OnTimeSetListener {
268 public TimeListener(View view) {
272 public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
273 // Cache the member variables locally to avoid inner class overhead.
274 Time startTime = mStartTime;
275 Time endTime = mEndTime;
277 // Cache the start and end millis so that we limit the number
278 // of calls to normalize() and toMillis(), which are fairly
282 if (mView == mStartTimeButton) {
283 // The start time was changed.
284 int hourDuration = endTime.hour - startTime.hour;
285 int minuteDuration = endTime.minute - startTime.minute;
287 startTime.hour = hourOfDay;
288 startTime.minute = minute;
289 startMillis = startTime.normalize(true);
291 // Also update the end time to keep the duration constant.
292 endTime.hour = hourOfDay + hourDuration;
293 endTime.minute = minute + minuteDuration;
295 // The end time was changed.
296 startMillis = startTime.toMillis(true);
297 endTime.hour = hourOfDay;
298 endTime.minute = minute;
300 // Move to the next day if the end time is before the start time.
301 if (endTime.before(startTime)) {
302 endTime.monthDay = startTime.monthDay + 1;
306 endMillis = endTime.normalize(true);
308 setDate(mEndDateButton, endMillis);
309 setTime(mStartTimeButton, startMillis);
310 setTime(mEndTimeButton, endMillis);
314 private class TimeClickListener implements View.OnClickListener {
317 public TimeClickListener(Time time) {
321 public void onClick(View v) {
322 new TimePickerDialog(EditEvent.this, new TimeListener(v),
323 mTime.hour, mTime.minute,
324 DateFormat.is24HourFormat(EditEvent.this)).show();
328 private class DateListener implements OnDateSetListener {
331 public DateListener(View view) {
335 public void onDateSet(DatePicker view, int year, int month, int monthDay) {
336 // Cache the member variables locally to avoid inner class overhead.
337 Time startTime = mStartTime;
338 Time endTime = mEndTime;
340 // Cache the start and end millis so that we limit the number
341 // of calls to normalize() and toMillis(), which are fairly
345 if (mView == mStartDateButton) {
346 // The start date was changed.
347 int yearDuration = endTime.year - startTime.year;
348 int monthDuration = endTime.month - startTime.month;
349 int monthDayDuration = endTime.monthDay - startTime.monthDay;
351 startTime.year = year;
352 startTime.month = month;
353 startTime.monthDay = monthDay;
354 startMillis = startTime.normalize(true);
356 // Also update the end date to keep the duration constant.
357 endTime.year = year + yearDuration;
358 endTime.month = month + monthDuration;
359 endTime.monthDay = monthDay + monthDayDuration;
360 endMillis = endTime.normalize(true);
362 // If the start date has changed then update the repeats.
365 // The end date was changed.
366 startMillis = startTime.toMillis(true);
368 endTime.month = month;
369 endTime.monthDay = monthDay;
370 endMillis = endTime.normalize(true);
372 // Do not allow an event to have an end time before the start time.
373 if (endTime.before(startTime)) {
374 endTime.set(startTime);
375 endMillis = startMillis;
379 setDate(mStartDateButton, startMillis);
380 setDate(mEndDateButton, endMillis);
381 setTime(mEndTimeButton, endMillis); // In case end time had to be reset
385 private class DateClickListener implements View.OnClickListener {
388 public DateClickListener(Time time) {
392 public void onClick(View v) {
393 new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year,
394 mTime.month, mTime.monthDay).show();
398 static private class CalendarsAdapter extends ResourceCursorAdapter {
399 public CalendarsAdapter(Context context, Cursor c) {
400 super(context, R.layout.calendars_item, c);
401 setDropDownViewResource(R.layout.calendars_dropdown_item);
405 public void bindView(View view, Context context, Cursor cursor) {
406 View colorBar = view.findViewById(R.id.color);
407 if (colorBar != null) {
408 colorBar.setBackgroundDrawable(
409 Utils.getColorChip(cursor.getInt(CALENDARS_INDEX_COLOR)));
412 TextView name = (TextView) view.findViewById(R.id.calendar_name);
414 String displayName = cursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
415 name.setText(displayName);
416 name.setTextColor(0xFF000000);
418 TextView accountName = (TextView) view.findViewById(R.id.account_name);
419 if(accountName != null) {
420 Resources res = context.getResources();
421 accountName.setText(cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT));
422 accountName.setVisibility(TextView.VISIBLE);
423 accountName.setTextColor(res.getColor(R.color.calendar_owner_text_color));
429 // This is called if the user clicks on one of the buttons: "Save",
430 // "Discard", or "Delete". This is also called if the user clicks
431 // on the "remove reminder" button.
432 public void onClick(View v) {
433 if (v == mSaveButton) {
440 if (v == mDeleteButton) {
441 long begin = mStartTime.toMillis(false /* use isDst */);
442 long end = mEndTime.toMillis(false /* use isDst */);
444 switch (mModification) {
445 case MODIFY_SELECTED:
446 which = DeleteEventHelper.DELETE_SELECTED;
448 case MODIFY_ALL_FOLLOWING:
449 which = DeleteEventHelper.DELETE_ALL_FOLLOWING;
452 which = DeleteEventHelper.DELETE_ALL;
455 mDeleteEventHelper.delete(begin, end, mEventCursor, which);
459 if (v == mDiscardButton) {
464 // This must be a click on one of the "remove reminder" buttons
465 LinearLayout reminderItem = (LinearLayout) v.getParent();
466 LinearLayout parent = (LinearLayout) reminderItem.getParent();
467 parent.removeView(reminderItem);
468 mReminderItems.remove(reminderItem);
469 updateRemindersVisibility();
472 // This is called if the user cancels a popup dialog. There are two
473 // dialogs: the "Loading calendars" dialog, and the "No calendars"
474 // dialog. The "Loading calendars" dialog is shown if there is a delay
475 // in loading the calendars (needed when creating an event) and the user
476 // tries to save the event before the calendars have finished loading.
477 // The "No calendars" dialog is shown if there are no syncable calendars.
478 public void onCancel(DialogInterface dialog) {
479 if (dialog == mLoadingCalendarsDialog) {
480 mSaveAfterQueryComplete = false;
481 } else if (dialog == mNoCalendarsDialog) {
486 // This is called if the user clicks on a dialog button.
487 public void onClick(DialogInterface dialog, int which) {
488 if (dialog == mNoCalendarsDialog) {
493 private class QueryHandler extends AsyncQueryHandler {
494 public QueryHandler(ContentResolver cr) {
499 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
500 // If the query didn't return a cursor for some reason return
501 if (cursor == null) {
505 // If the Activity is finishing, then close the cursor.
506 // Otherwise, use the new cursor in the adapter.
508 stopManagingCursor(cursor);
511 mCalendarsCursor = cursor;
512 startManagingCursor(cursor);
515 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
516 Window.PROGRESS_VISIBILITY_OFF);
518 // If there are no syncable calendars, then we cannot allow
519 // creating a new event.
520 if (cursor.getCount() == 0) {
521 // Cancel the "loading calendars" dialog if it exists
522 if (mSaveAfterQueryComplete) {
523 mLoadingCalendarsDialog.cancel();
526 // Create an error message for the user that, when clicked,
527 // will exit this activity without saving the event.
528 AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
529 builder.setTitle(R.string.no_syncable_calendars)
530 .setIcon(android.R.drawable.ic_dialog_alert)
531 .setMessage(R.string.no_calendars_found)
532 .setPositiveButton(android.R.string.ok, EditEvent.this)
533 .setOnCancelListener(EditEvent.this);
534 mNoCalendarsDialog = builder.show();
538 int defaultCalendarPosition = findDefaultCalendarPosition(mCalendarsCursor);
540 // populate the calendars spinner
541 CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor);
542 mCalendarsSpinner.setAdapter(adapter);
543 mCalendarsSpinner.setSelection(defaultCalendarPosition);
544 mCalendarsQueryComplete = true;
545 if (mSaveAfterQueryComplete) {
546 mLoadingCalendarsDialog.cancel();
551 // Find user domain and set it to the validator.
552 // TODO: we may want to update this validator if the user actually picks
553 // a different calendar. maybe not. depends on what we want for the
554 // user experience. this may change when we add support for multiple
556 if (mHasAttendeeData && cursor.moveToPosition(defaultCalendarPosition)) {
557 String ownEmail = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
558 if (ownEmail != null) {
559 String domain = extractDomain(ownEmail);
560 if (domain != null) {
561 mEmailValidator = new Rfc822Validator(domain);
562 mAttendeesList.setValidator(mEmailValidator);
569 // Find the calendar position in the cursor that matches calendar in preference
570 private int findDefaultCalendarPosition(Cursor calendarsCursor) {
571 if (calendarsCursor.getCount() <= 0) {
575 String defaultCalendar = Utils.getSharedPreference(EditEvent.this,
576 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, null);
578 if (defaultCalendar == null) {
583 calendarsCursor.moveToPosition(-1);
584 while(calendarsCursor.moveToNext()) {
585 if (defaultCalendar.equals(mCalendarsCursor
586 .getString(CALENDARS_INDEX_OWNER_ACCOUNT))) {
595 private static String extractDomain(String email) {
596 int separator = email.lastIndexOf('@');
597 if (separator != -1 && ++separator < email.length()) {
598 return email.substring(separator);
604 protected void onCreate(Bundle icicle) {
605 super.onCreate(icicle);
606 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
607 setContentView(R.layout.edit_event);
609 boolean newEvent = false;
611 mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
613 mStartTime = new Time();
614 mEndTime = new Time();
616 Intent intent = getIntent();
617 mUri = intent.getData();
620 mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null, null);
621 if (mEventCursor == null || mEventCursor.getCount() == 0) {
622 // The cursor is empty. This can happen if the event was deleted.
628 long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
629 long end = intent.getLongExtra(EVENT_END_TIME, 0);
631 String domain = "gmail.com";
633 boolean allDay = false;
634 if (mEventCursor != null) {
635 // The event already exists so fetch the all-day status
636 mEventCursor.moveToFirst();
637 mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
638 allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
639 String rrule = mEventCursor.getString(EVENT_INDEX_RRULE);
640 String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
641 long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
642 mOwnerAccount = mEventCursor.getString(EVENT_INDEX_OWNER_ACCOUNT);
643 if (!TextUtils.isEmpty(mOwnerAccount)) {
644 String ownerDomain = extractDomain(mOwnerAccount);
645 if (ownerDomain != null) {
646 domain = ownerDomain;
650 // Remember the initial values
651 mInitialValues = new ContentValues();
652 mInitialValues.put(EVENT_BEGIN_TIME, begin);
653 mInitialValues.put(EVENT_END_TIME, end);
654 mInitialValues.put(Events.ALL_DAY, allDay ? 1 : 0);
655 mInitialValues.put(Events.RRULE, rrule);
656 mInitialValues.put(Events.EVENT_TIMEZONE, timezone);
657 mInitialValues.put(Events.CALENDAR_ID, calendarId);
660 // We are creating a new event, so set the default from the
661 // intent (if specified).
662 allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false);
665 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
666 Window.PROGRESS_VISIBILITY_ON);
668 // Start a query in the background to read the list of calendars
669 mQueryHandler = new QueryHandler(getContentResolver());
670 mQueryHandler.startQuery(0, null, Calendars.CONTENT_URI, CALENDARS_PROJECTION,
671 CALENDARS_WHERE, null /* selection args */, null /* sort order */);
674 // If the event is all-day, read the times in UTC timezone
677 String tz = mStartTime.timezone;
678 mStartTime.timezone = Time.TIMEZONE_UTC;
679 mStartTime.set(begin);
680 mStartTime.timezone = tz;
682 // Calling normalize to calculate isDst
683 mStartTime.normalize(true);
685 mStartTime.set(begin);
691 String tz = mStartTime.timezone;
692 mEndTime.timezone = Time.TIMEZONE_UTC;
694 mEndTime.timezone = tz;
696 // Calling normalize to calculate isDst
697 mEndTime.normalize(true);
703 // cache all the widgets
704 mTitleTextView = (TextView) findViewById(R.id.title);
705 mLocationTextView = (TextView) findViewById(R.id.location);
706 mDescriptionTextView = (TextView) findViewById(R.id.description);
707 mStartDateButton = (Button) findViewById(R.id.start_date);
708 mEndDateButton = (Button) findViewById(R.id.end_date);
709 mStartTimeButton = (Button) findViewById(R.id.start_time);
710 mEndTimeButton = (Button) findViewById(R.id.end_time);
711 mAllDayCheckBox = (CheckBox) findViewById(R.id.is_all_day);
712 mCalendarsSpinner = (Spinner) findViewById(R.id.calendars);
713 mRepeatsSpinner = (Spinner) findViewById(R.id.repeats);
714 mAvailabilitySpinner = (Spinner) findViewById(R.id.availability);
715 mVisibilitySpinner = (Spinner) findViewById(R.id.visibility);
716 mRemindersSeparator = findViewById(R.id.reminders_separator);
717 mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
718 mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
720 if (mHasAttendeeData) {
721 mAddressAdapter = new EmailAddressAdapter(this);
722 mEmailValidator = new Rfc822Validator(domain);
723 mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees);
725 findViewById(R.id.attendees_group).setVisibility(View.GONE);
728 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
729 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
731 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
733 long endMillis = mEndTime.normalize(true);
735 // Do not allow an event to have an end time before the start time.
736 if (mEndTime.before(mStartTime)) {
737 mEndTime.set(mStartTime);
738 endMillis = mEndTime.normalize(true);
740 setDate(mEndDateButton, endMillis);
741 setTime(mEndTimeButton, endMillis);
744 mStartTimeButton.setVisibility(View.GONE);
745 mEndTimeButton.setVisibility(View.GONE);
747 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
749 long endMillis = mEndTime.normalize(true);
750 setDate(mEndDateButton, endMillis);
751 setTime(mEndTimeButton, endMillis);
754 mStartTimeButton.setVisibility(View.VISIBLE);
755 mEndTimeButton.setVisibility(View.VISIBLE);
761 mAllDayCheckBox.setChecked(true);
763 mAllDayCheckBox.setChecked(false);
766 mSaveButton = (Button) findViewById(R.id.save);
767 mSaveButton.setOnClickListener(this);
769 mDeleteButton = (Button) findViewById(R.id.delete);
770 mDeleteButton.setOnClickListener(this);
772 mDiscardButton = (Button) findViewById(R.id.discard);
773 mDiscardButton.setOnClickListener(this);
775 // Initialize the reminder values array.
776 Resources r = getResources();
777 String[] strings = r.getStringArray(R.array.reminder_minutes_values);
778 int size = strings.length;
779 ArrayList<Integer> list = new ArrayList<Integer>(size);
780 for (int i = 0 ; i < size ; i++) {
781 list.add(Integer.parseInt(strings[i]));
783 mReminderValues = list;
784 String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
785 mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
787 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(this);
788 String durationString =
789 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
790 mDefaultReminderMinutes = Integer.parseInt(durationString);
792 if (newEvent && mDefaultReminderMinutes != 0) {
793 addReminder(this, this, mReminderItems, mReminderValues,
794 mReminderLabels, mDefaultReminderMinutes);
797 long eventId = (mEventCursor == null) ? -1 : mEventCursor.getLong(EVENT_INDEX_ID);
798 ContentResolver cr = getContentResolver();
801 boolean hasAlarm = (mEventCursor != null)
802 && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0);
804 Uri uri = Reminders.CONTENT_URI;
805 String where = String.format(REMINDERS_WHERE, eventId);
806 Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
808 // First pass: collect all the custom reminder minutes (e.g.,
809 // a reminder of 8 minutes) into a global list.
810 while (reminderCursor.moveToNext()) {
811 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
812 EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
815 // Second pass: create the reminder spinners
816 reminderCursor.moveToPosition(-1);
817 while (reminderCursor.moveToNext()) {
818 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
819 mOriginalMinutes.add(minutes);
820 EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
821 mReminderLabels, minutes);
824 reminderCursor.close();
827 updateRemindersVisibility();
829 // Setup the + Add Reminder Button
830 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
831 public void onClick(View v) {
835 ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
836 reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
838 mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
841 if (mHasAttendeeData && eventId != -1) {
842 Uri uri = Attendees.CONTENT_URI;
843 String[] whereArgs = {Long.toString(eventId)};
844 Cursor attendeeCursor = cr.query(uri, ATTENDEES_PROJECTION, ATTENDEES_WHERE, whereArgs,
847 StringBuilder b = new StringBuilder();
848 while (attendeeCursor.moveToNext()) {
849 String name = attendeeCursor.getString(ATTENDEES_INDEX_NAME);
850 String email = attendeeCursor.getString(ATTENDEES_INDEX_EMAIL);
852 if (name != null && name.length() > 0 && !name.equals(email)) {
853 b.append('"').append(name).append("\" ");
855 b.append('<').append(email).append(">, ");
858 if (b.length() > 0) {
859 mOriginalAttendees = b.toString();
860 mAttendeesList.setText(mOriginalAttendees);
863 attendeeCursor.close();
866 if (mEventCursor == null) {
867 // Allow the intent to specify the fields in the event.
868 // This will allow other apps to create events easily.
869 initFromIntent(intent);
873 private LinkedHashSet<Rfc822Token> getAddressesFromList(MultiAutoCompleteTextView list) {
874 list.clearComposingText();
875 LinkedHashSet<Rfc822Token> addresses = new LinkedHashSet<Rfc822Token>();
876 Rfc822Tokenizer.tokenize(list.getText(), addresses);
878 // validate the emails, out of paranoia. they should already be
879 // validated on input, but drop any invalid emails just to be safe.
880 Iterator<Rfc822Token> addressIterator = addresses.iterator();
881 while (addressIterator.hasNext()) {
882 Rfc822Token address = addressIterator.next();
883 if (!mEmailValidator.isValid(address.getAddress())) {
884 Log.w(TAG, "Dropping invalid attendee email address: " + address);
885 addressIterator.remove();
891 // From com.google.android.gm.ComposeActivity
892 private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) {
893 MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res);
894 list.setAdapter(mAddressAdapter);
895 list.setTokenizer(new Rfc822Tokenizer());
896 list.setValidator(mEmailValidator);
898 // NOTE: assumes no other filters are set
899 list.setFilters(sRecipientFilters);
905 * From com.google.android.gm.ComposeActivity
906 * Implements special address cleanup rules:
907 * The first space key entry following an "@" symbol that is followed by any combination
908 * of letters and symbols, including one+ dots and zero commas, should insert an extra
909 * comma (followed by the space).
911 private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() };
913 private void initFromIntent(Intent intent) {
914 String title = intent.getStringExtra(Events.TITLE);
916 mTitleTextView.setText(title);
919 String location = intent.getStringExtra(Events.EVENT_LOCATION);
920 if (location != null) {
921 mLocationTextView.setText(location);
924 String description = intent.getStringExtra(Events.DESCRIPTION);
925 if (description != null) {
926 mDescriptionTextView.setText(description);
929 int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
930 if (availability != -1) {
931 mAvailabilitySpinner.setSelection(availability);
934 int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
935 if (visibility != -1) {
936 mVisibilitySpinner.setSelection(visibility);
939 String rrule = intent.getStringExtra(Events.RRULE);
942 mEventRecurrence.parse(rrule);
947 protected void onResume() {
951 if (mEventCursor == null || mEventCursor.getCount() == 0) {
952 // The cursor is empty. This can happen if the event was deleted.
958 if (mEventCursor != null) {
959 Cursor cursor = mEventCursor;
960 cursor.moveToFirst();
962 mRrule = cursor.getString(EVENT_INDEX_RRULE);
963 String title = cursor.getString(EVENT_INDEX_TITLE);
964 String description = cursor.getString(EVENT_INDEX_DESCRIPTION);
965 String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
966 int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY);
967 int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY);
968 if (visibility > 0) {
969 // For now we the array contains the values 0, 2, and 3. We subtract one to match.
973 if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) {
974 // If this event has not been synced, then don't allow deleting
975 // or changing a single instance.
976 mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
977 mEventRecurrence.parse(mRrule);
979 // If we haven't synced this repeating event yet, then don't
980 // allow the user to change just one instance.
982 CharSequence[] items;
983 if (mSyncId == null) {
984 if(isFirstEventInSeries()) {
985 // Still display the option so the user knows all events are changing
986 items = new CharSequence[1];
988 items = new CharSequence[2];
991 if(isFirstEventInSeries()) {
992 items = new CharSequence[2];
994 items = new CharSequence[3];
996 items[itemIndex++] = getText(R.string.modify_event);
998 items[itemIndex++] = getText(R.string.modify_all);
1000 // Do one more check to make sure this remains at the end of the list
1001 if(!isFirstEventInSeries()) {
1002 // TODO Find out why modify all following causes a dup of the first event if
1003 // it's operating on the first event.
1004 items[itemIndex++] = getText(R.string.modify_all_following);
1007 // Display the modification dialog.
1008 new AlertDialog.Builder(this)
1009 .setOnCancelListener(new OnCancelListener() {
1010 public void onCancel(DialogInterface dialog) {
1014 .setTitle(R.string.edit_event_label)
1015 .setItems(items, new OnClickListener() {
1016 public void onClick(DialogInterface dialog, int which) {
1019 (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED;
1020 } else if (which == 1) {
1022 (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL;
1023 } else if (which == 2) {
1024 mModification = MODIFY_ALL_FOLLOWING;
1027 // If we are modifying all the events in a
1028 // series then disable and ignore the date.
1029 if (mModification == MODIFY_ALL) {
1030 mStartDateButton.setEnabled(false);
1031 mEndDateButton.setEnabled(false);
1032 } else if (mModification == MODIFY_SELECTED) {
1033 mRepeatsSpinner.setEnabled(false);
1040 mTitleTextView.setText(title);
1041 mLocationTextView.setText(location);
1042 mDescriptionTextView.setText(description);
1043 mAvailabilitySpinner.setSelection(availability);
1044 mVisibilitySpinner.setSelection(visibility);
1046 // This is an existing event so hide the calendar spinner
1047 // since we can't change the calendar.
1048 View calendarGroup = findViewById(R.id.calendar_group);
1049 calendarGroup.setVisibility(View.GONE);
1052 if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) {
1053 mStartTime.setToNow();
1055 // Round the time to the nearest half hour.
1056 mStartTime.second = 0;
1057 int minute = mStartTime.minute;
1059 // We are already on a half hour increment
1060 } else if (minute > 0 && minute <= 30) {
1061 mStartTime.minute = 30;
1063 mStartTime.minute = 0;
1064 mStartTime.hour += 1;
1067 long startMillis = mStartTime.normalize(true /* ignore isDst */);
1068 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
1071 // Hide delete button
1072 mDeleteButton.setVisibility(View.GONE);
1075 updateRemindersVisibility();
1081 public boolean onCreateOptionsMenu(Menu menu) {
1083 item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
1084 R.string.add_new_reminder);
1085 item.setIcon(R.drawable.ic_menu_reminder);
1086 item.setAlphabeticShortcut('r');
1088 item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
1089 R.string.edit_event_show_extra_options);
1090 item.setIcon(R.drawable.ic_menu_show_list);
1091 item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
1092 R.string.edit_event_hide_extra_options);
1093 item.setIcon(R.drawable.ic_menu_show_list);
1095 return super.onCreateOptionsMenu(menu);
1099 public boolean onPrepareOptionsMenu(Menu menu) {
1100 if (mReminderItems.size() < MAX_REMINDERS) {
1101 menu.setGroupVisible(MENU_GROUP_REMINDER, true);
1102 menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
1104 menu.setGroupVisible(MENU_GROUP_REMINDER, false);
1105 menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
1108 if (mExtraOptions.getVisibility() == View.VISIBLE) {
1109 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
1110 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
1112 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
1113 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
1116 return super.onPrepareOptionsMenu(menu);
1119 private void addReminder() {
1120 // TODO: when adding a new reminder, make it different from the
1121 // last one in the list (if any).
1122 if (mDefaultReminderMinutes == 0) {
1123 addReminder(this, this, mReminderItems, mReminderValues,
1124 mReminderLabels, 10 /* minutes */);
1126 addReminder(this, this, mReminderItems, mReminderValues,
1127 mReminderLabels, mDefaultReminderMinutes);
1129 updateRemindersVisibility();
1133 public boolean onOptionsItemSelected(MenuItem item) {
1134 switch (item.getItemId()) {
1135 case MENU_ADD_REMINDER:
1138 case MENU_SHOW_EXTRA_OPTIONS:
1139 mExtraOptions.setVisibility(View.VISIBLE);
1141 case MENU_HIDE_EXTRA_OPTIONS:
1142 mExtraOptions.setVisibility(View.GONE);
1145 return super.onOptionsItemSelected(item);
1149 public void onBackPressed() {
1150 // If we are creating a new event, do not create it if the
1151 // title, location and description are all empty, in order to
1152 // prevent accidental "no subject" event creations.
1153 if (mUri != null || !isEmpty()) {
1155 // We cannot exit this activity because the calendars
1156 // are still loading.
1163 private void populateWhen() {
1164 long startMillis = mStartTime.toMillis(false /* use isDst */);
1165 long endMillis = mEndTime.toMillis(false /* use isDst */);
1166 setDate(mStartDateButton, startMillis);
1167 setDate(mEndDateButton, endMillis);
1169 setTime(mStartTimeButton, startMillis);
1170 setTime(mEndTimeButton, endMillis);
1172 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
1173 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
1175 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
1176 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
1179 private void populateRepeats() {
1180 Time time = mStartTime;
1181 Resources r = getResources();
1182 int resource = android.R.layout.simple_spinner_item;
1184 String[] days = new String[] {
1185 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
1186 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
1187 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
1188 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
1189 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
1190 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
1191 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
1193 String[] ordinals = r.getStringArray(R.array.ordinal_labels);
1195 // Only display "Custom" in the spinner if the device does not support the
1196 // recurrence functionality of the event. Only display every weekday if
1197 // the event starts on a weekday.
1198 boolean isCustomRecurrence = isCustomRecurrence();
1199 boolean isWeekdayEvent = isWeekdayEvent();
1201 ArrayList<String> repeatArray = new ArrayList<String>(0);
1202 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
1204 repeatArray.add(r.getString(R.string.does_not_repeat));
1205 recurrenceIndexes.add(DOES_NOT_REPEAT);
1207 repeatArray.add(r.getString(R.string.daily));
1208 recurrenceIndexes.add(REPEATS_DAILY);
1210 if (isWeekdayEvent) {
1211 repeatArray.add(r.getString(R.string.every_weekday));
1212 recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
1215 String format = r.getString(R.string.weekly);
1216 repeatArray.add(String.format(format, time.format("%A")));
1217 recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
1219 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
1220 int dayNumber = (time.monthDay - 1) / 7;
1221 format = r.getString(R.string.monthly_on_day_count);
1222 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
1223 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
1225 format = r.getString(R.string.monthly_on_day);
1226 repeatArray.add(String.format(format, time.monthDay));
1227 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
1229 long when = time.toMillis(false);
1230 format = r.getString(R.string.yearly);
1232 if (DateFormat.is24HourFormat(this)) {
1233 flags |= DateUtils.FORMAT_24HOUR;
1235 repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
1236 recurrenceIndexes.add(REPEATS_YEARLY);
1238 if (isCustomRecurrence) {
1239 repeatArray.add(r.getString(R.string.custom));
1240 recurrenceIndexes.add(REPEATS_CUSTOM);
1242 mRecurrenceIndexes = recurrenceIndexes;
1244 int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
1245 if (mRrule != null) {
1246 if (isCustomRecurrence) {
1247 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
1249 switch (mEventRecurrence.freq) {
1250 case EventRecurrence.DAILY:
1251 position = recurrenceIndexes.indexOf(REPEATS_DAILY);
1253 case EventRecurrence.WEEKLY:
1254 if (mEventRecurrence.repeatsOnEveryWeekDay()) {
1255 position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
1257 position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
1260 case EventRecurrence.MONTHLY:
1261 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1262 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
1264 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
1267 case EventRecurrence.YEARLY:
1268 position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
1273 ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
1274 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1275 mRepeatsSpinner.setAdapter(adapter);
1276 mRepeatsSpinner.setSelection(position);
1279 // Adds a reminder to the displayed list of reminders.
1280 // Returns true if successfully added reminder, false if no reminders can
1282 static boolean addReminder(Activity activity, View.OnClickListener listener,
1283 ArrayList<LinearLayout> items, ArrayList<Integer> values,
1284 ArrayList<String> labels, int minutes) {
1286 if (items.size() >= MAX_REMINDERS) {
1290 LayoutInflater inflater = activity.getLayoutInflater();
1291 LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
1292 LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
1293 parent.addView(reminderItem);
1295 Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
1296 Resources res = activity.getResources();
1297 spinner.setPrompt(res.getString(R.string.reminders_label));
1298 int resource = android.R.layout.simple_spinner_item;
1299 ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
1300 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1301 spinner.setAdapter(adapter);
1303 ImageButton reminderRemoveButton;
1304 reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
1305 reminderRemoveButton.setOnClickListener(listener);
1307 int index = findMinutesInReminderList(values, minutes);
1308 spinner.setSelection(index);
1309 items.add(reminderItem);
1314 static void addMinutesToList(Context context, ArrayList<Integer> values,
1315 ArrayList<String> labels, int minutes) {
1316 int index = values.indexOf(minutes);
1321 // The requested "minutes" does not exist in the list, so insert it
1324 String label = constructReminderLabel(context, minutes, false);
1325 int len = values.size();
1326 for (int i = 0; i < len; i++) {
1327 if (minutes < values.get(i)) {
1328 values.add(i, minutes);
1329 labels.add(i, label);
1334 values.add(minutes);
1335 labels.add(len, label);
1339 * Finds the index of the given "minutes" in the "values" list.
1341 * @param values the list of minutes corresponding to the spinner choices
1342 * @param minutes the minutes to search for in the values list
1343 * @return the index of "minutes" in the "values" list
1345 private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
1346 int index = values.indexOf(minutes);
1348 // This should never happen.
1349 Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
1355 // Constructs a label given an arbitrary number of minutes. For example,
1356 // if the given minutes is 63, then this returns the string "63 minutes".
1357 // As another example, if the given minutes is 120, then this returns
1359 static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
1360 Resources resources = context.getResources();
1363 if (minutes % 60 != 0) {
1366 resId = R.plurals.Nmins;
1368 resId = R.plurals.Nminutes;
1370 } else if (minutes % (24 * 60) != 0) {
1371 value = minutes / 60;
1372 resId = R.plurals.Nhours;
1374 value = minutes / ( 24 * 60);
1375 resId = R.plurals.Ndays;
1378 String format = resources.getQuantityString(resId, value);
1379 return String.format(format, value);
1382 private void updateRemindersVisibility() {
1383 if (mReminderItems.size() == 0) {
1384 mRemindersSeparator.setVisibility(View.GONE);
1385 mRemindersContainer.setVisibility(View.GONE);
1387 mRemindersSeparator.setVisibility(View.VISIBLE);
1388 mRemindersContainer.setVisibility(View.VISIBLE);
1392 private void setDate(TextView view, long millis) {
1393 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
1394 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
1395 DateUtils.FORMAT_ABBREV_WEEKDAY;
1396 view.setText(DateUtils.formatDateTime(this, millis, flags));
1399 private void setTime(TextView view, long millis) {
1400 int flags = DateUtils.FORMAT_SHOW_TIME;
1401 if (DateFormat.is24HourFormat(this)) {
1402 flags |= DateUtils.FORMAT_24HOUR;
1404 view.setText(DateUtils.formatDateTime(this, millis, flags));
1407 // Saves the event. Returns true if it is okay to exit this activity.
1408 private boolean save() {
1409 boolean forceSaveReminders = false;
1411 // If we are creating a new event, then make sure we wait until the
1412 // query to fetch the list of calendars has finished.
1413 if (mEventCursor == null) {
1414 if (!mCalendarsQueryComplete) {
1415 // Wait for the calendars query to finish.
1416 if (mLoadingCalendarsDialog == null) {
1417 // Create the progress dialog
1418 mLoadingCalendarsDialog = ProgressDialog.show(this,
1419 getText(R.string.loading_calendars_title),
1420 getText(R.string.loading_calendars_message),
1422 mSaveAfterQueryComplete = true;
1427 // Avoid creating a new event if the calendars cursor is empty or we clicked through
1428 // too quickly and no calendar was selected (blame the monkey)
1429 if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0 ||
1430 mCalendarsSpinner.getSelectedItemId() == AdapterView.INVALID_ROW_ID) {
1431 Log.w("Cal", "The calendars table does not contain any calendars"
1432 + " or no calendar was selected."
1433 + " New event was not created.");
1436 Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
1438 Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
1441 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1442 int eventIdIndex = -1;
1444 ContentValues values = getContentValuesFromUi();
1447 // Update the "hasAlarm" field for the event
1448 ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
1450 int len = reminderMinutes.size();
1451 values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
1453 // For recurring events, we must make sure that we use duration rather
1456 // Add hasAttendeeData for a new event
1457 values.put(Events.HAS_ATTENDEE_DATA, 1);
1458 // Create new event with new contents
1459 addRecurrenceRule(values);
1460 if (mRrule != null) {
1461 values.remove(Events.DTEND);
1463 eventIdIndex = ops.size();
1464 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1466 forceSaveReminders = true;
1468 } else if (mRrule == null) {
1469 // Modify contents of a non-repeating event
1470 addRecurrenceRule(values);
1471 checkTimeDependentFields(values);
1472 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1474 } else if (mInitialValues.getAsString(Events.RRULE) == null) {
1475 // This event was changed from a non-repeating event to a
1477 addRecurrenceRule(values);
1478 values.remove(Events.DTEND);
1479 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1481 } else if (mModification == MODIFY_SELECTED) {
1482 // Modify contents of the current instance of repeating event
1484 // Create a recurrence exception
1485 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1486 values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
1487 values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
1488 boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1489 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
1491 eventIdIndex = ops.size();
1492 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1494 forceSaveReminders = true;
1496 } else if (mModification == MODIFY_ALL_FOLLOWING) {
1497 // Modify this instance and all future instances of repeating event
1498 addRecurrenceRule(values);
1500 if (mRrule == null) {
1501 // We've changed a recurring event to a non-recurring event.
1502 // If the event we are editing is the first in the series,
1503 // then delete the whole series. Otherwise, update the series
1504 // to end at the new start time.
1505 if (isFirstEventInSeries()) {
1506 ops.add(ContentProviderOperation.newDelete(uri).build());
1508 // Update the current repeating event to end at the new
1510 updatePastEvents(ops, uri);
1512 eventIdIndex = ops.size();
1513 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1516 if (isFirstEventInSeries()) {
1517 checkTimeDependentFields(values);
1518 values.remove(Events.DTEND);
1519 Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
1522 // Update the current repeating event to end at the new
1524 updatePastEvents(ops, uri);
1526 // Create a new event with the user-modified fields
1527 values.remove(Events.DTEND);
1528 eventIdIndex = ops.size();
1529 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
1533 forceSaveReminders = true;
1535 } else if (mModification == MODIFY_ALL) {
1537 // Modify all instances of repeating event
1538 addRecurrenceRule(values);
1540 if (mRrule == null) {
1541 // We've changed a recurring event to a non-recurring event.
1542 // Delete the whole series and replace it with a new
1543 // non-recurring event.
1544 ops.add(ContentProviderOperation.newDelete(uri).build());
1546 eventIdIndex = ops.size();
1547 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1549 forceSaveReminders = true;
1551 checkTimeDependentFields(values);
1552 values.remove(Events.DTEND);
1553 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1557 // New Event or New Exception to an existing event
1558 boolean newEvent = (eventIdIndex != -1);
1561 saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
1562 forceSaveReminders);
1563 } else if (uri != null) {
1564 long eventId = ContentUris.parseId(uri);
1565 saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
1566 forceSaveReminders);
1571 // New event/instance - Set Organizer's response as yes
1572 if (mHasAttendeeData && newEvent) {
1574 int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
1576 // Save the default calendar for new events
1577 if (mCalendarsCursor != null) {
1578 if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
1579 String defaultCalendar = mCalendarsCursor
1580 .getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1581 Utils.setSharedPreference(this,
1582 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, defaultCalendar);
1586 String ownerEmail = mOwnerAccount;
1587 // Just in case mOwnerAccount is null, try to get owner from mCalendarsCursor
1588 if (ownerEmail == null && mCalendarsCursor != null &&
1589 mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
1590 ownerEmail = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1592 if (ownerEmail != null) {
1593 values.put(Attendees.ATTENDEE_EMAIL, ownerEmail);
1594 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
1595 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1596 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
1598 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1599 .withValues(values);
1600 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1605 // TODO: is this the right test? this currently checks if this is
1606 // a new event or an existing event. or is this a paranoia check?
1607 if (mHasAttendeeData && (newEvent || uri != null)) {
1608 Editable attendeesText = mAttendeesList.getText();
1609 // Hit the content provider only if this is a new event or the user has changed it
1610 if (newEvent || !mOriginalAttendees.equals(attendeesText.toString())) {
1611 // figure out which attendees need to be added and which ones
1612 // need to be deleted. use a linked hash set, so we maintain
1613 // order (but also remove duplicates).
1614 LinkedHashSet<Rfc822Token> newAttendees = getAddressesFromList(mAttendeesList);
1616 // the eventId is only used if eventIdIndex is -1.
1617 // TODO: clean up this code.
1618 long eventId = uri != null ? ContentUris.parseId(uri) : -1;
1620 // only compute deltas if this is an existing event.
1621 // new events (being inserted into the Events table) won't
1622 // have any existing attendees.
1624 HashSet<Rfc822Token> removedAttendees = new HashSet<Rfc822Token>();
1625 HashSet<Rfc822Token> originalAttendees = new HashSet<Rfc822Token>();
1626 Rfc822Tokenizer.tokenize(mOriginalAttendees, originalAttendees);
1627 for (Rfc822Token originalAttendee : originalAttendees) {
1628 if (newAttendees.contains(originalAttendee)) {
1629 // existing attendee. remove from new attendees set.
1630 newAttendees.remove(originalAttendee);
1632 // no longer in attendees. mark as removed.
1633 removedAttendees.add(originalAttendee);
1637 // delete removed attendees
1638 b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
1640 String[] args = new String[removedAttendees.size() + 1];
1641 args[0] = Long.toString(eventId);
1643 StringBuilder deleteWhere = new StringBuilder(ATTENDEES_DELETE_PREFIX);
1644 for (Rfc822Token removedAttendee : removedAttendees) {
1646 deleteWhere.append(",");
1648 deleteWhere.append("?");
1649 args[i++] = removedAttendee.getAddress();
1651 deleteWhere.append(")");
1652 b.withSelection(deleteWhere.toString(), args);
1656 if (newAttendees.size() > 0) {
1657 // Insert the new attendees
1658 for (Rfc822Token attendee : newAttendees) {
1660 values.put(Attendees.ATTENDEE_NAME, attendee.getName());
1661 values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
1662 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
1663 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1664 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
1667 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1668 .withValues(values);
1669 b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
1671 values.put(Attendees.EVENT_ID, eventId);
1672 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1673 .withValues(values);
1682 // TODO Move this to background thread
1683 ContentProviderResult[] results =
1684 getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
1686 for (int i = 0; i < results.length; i++) {
1687 Log.v(TAG, "results = " + results[i].toString());
1690 } catch (RemoteException e) {
1691 Log.w(TAG, "Ignoring unexpected remote exception", e);
1692 } catch (OperationApplicationException e) {
1693 Log.w(TAG, "Ignoring unexpected exception", e);
1699 private boolean isFirstEventInSeries() {
1700 int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
1701 long start = mEventCursor.getLong(dtStart);
1702 return start == mStartTime.toMillis(true);
1705 private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) {
1706 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1707 String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
1708 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1709 String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
1710 mEventRecurrence.parse(oldRrule);
1712 Time untilTime = new Time();
1713 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1714 ContentValues oldValues = new ContentValues();
1716 // The "until" time must be in UTC time in order for Google calendar
1717 // to display it properly. For all-day events, the "until" time string
1718 // must include just the date field, and not the time field. The
1719 // repeating events repeat up to and including the "until" time.
1720 untilTime.timezone = Time.TIMEZONE_UTC;
1722 // Subtract one second from the old begin time to get the new
1724 untilTime.set(begin - 1000); // subtract one second (1000 millis)
1727 untilTime.minute = 0;
1728 untilTime.second = 0;
1729 untilTime.allDay = true;
1730 untilTime.normalize(false);
1732 // For all-day events, the duration must be in days, not seconds.
1733 // Otherwise, Google Calendar will (mistakenly) change this event
1734 // into a non-all-day event.
1735 int len = oldDuration.length();
1736 if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
1737 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
1738 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1739 oldDuration = "P" + days + "D";
1742 mEventRecurrence.until = untilTime.format2445();
1744 oldValues.put(Events.DTSTART, oldStartMillis);
1745 oldValues.put(Events.DURATION, oldDuration);
1746 oldValues.put(Events.RRULE, mEventRecurrence.toString());
1747 Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
1751 private void checkTimeDependentFields(ContentValues values) {
1752 long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1753 long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
1754 boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1755 String oldRrule = mInitialValues.getAsString(Events.RRULE);
1756 String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
1758 long newBegin = values.getAsLong(Events.DTSTART);
1759 long newEnd = values.getAsLong(Events.DTEND);
1760 boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
1761 String newRrule = values.getAsString(Events.RRULE);
1762 String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1764 // If none of the time-dependent fields changed, then remove them.
1765 if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
1766 && TextUtils.equals(oldRrule, newRrule)
1767 && TextUtils.equals(oldTimezone, newTimezone)) {
1768 values.remove(Events.DTSTART);
1769 values.remove(Events.DTEND);
1770 values.remove(Events.DURATION);
1771 values.remove(Events.ALL_DAY);
1772 values.remove(Events.RRULE);
1773 values.remove(Events.EVENT_TIMEZONE);
1777 if (oldRrule == null || newRrule == null) {
1781 // If we are modifying all events then we need to set DTSTART to the
1782 // start time of the first event in the series, not the current
1783 // date and time. If the start time of the event was changed
1784 // (from, say, 3pm to 4pm), then we want to add the time difference
1785 // to the start time of the first event in the series (the DTSTART
1786 // value). If we are modifying one instance or all following instances,
1787 // then we leave the DTSTART field alone.
1788 if (mModification == MODIFY_ALL) {
1789 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1790 if (oldBegin != newBegin) {
1791 // The user changed the start time of this event
1792 long offset = newBegin - oldBegin;
1793 oldStartMillis += offset;
1795 values.put(Events.DTSTART, oldStartMillis);
1799 static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
1800 ArrayList<Integer> reminderValues) {
1801 int len = reminderItems.size();
1802 ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
1803 for (int index = 0; index < len; index++) {
1804 LinearLayout layout = reminderItems.get(index);
1805 Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
1806 int minutes = reminderValues.get(spinner.getSelectedItemPosition());
1807 reminderMinutes.add(minutes);
1809 return reminderMinutes;
1813 * Saves the reminders, if they changed. Returns true if the database
1816 * @param ops the array of ContentProviderOperations
1817 * @param eventId the id of the event whose reminders are being updated
1818 * @param reminderMinutes the array of reminders set by the user
1819 * @param originalMinutes the original array of reminders
1820 * @param forceSave if true, then save the reminders even if they didn't
1822 * @return true if the database was updated
1824 static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
1825 ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
1826 boolean forceSave) {
1827 // If the reminders have not changed, then don't update the database
1828 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1832 // Delete all the existing reminders for this event
1833 String where = Reminders.EVENT_ID + "=?";
1834 String[] args = new String[] { Long.toString(eventId) };
1835 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1836 b.withSelection(where, args);
1839 ContentValues values = new ContentValues();
1840 int len = reminderMinutes.size();
1842 // Insert the new reminders, if any
1843 for (int i = 0; i < len; i++) {
1844 int minutes = reminderMinutes.get(i);
1847 values.put(Reminders.MINUTES, minutes);
1848 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1849 values.put(Reminders.EVENT_ID, eventId);
1850 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1856 static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
1857 int eventIdIndex, ArrayList<Integer> reminderMinutes,
1858 ArrayList<Integer> originalMinutes, boolean forceSave) {
1859 // If the reminders have not changed, then don't update the database
1860 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1864 // Delete all the existing reminders for this event
1865 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1866 b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
1867 b.withSelectionBackReference(0, eventIdIndex);
1870 ContentValues values = new ContentValues();
1871 int len = reminderMinutes.size();
1873 // Insert the new reminders, if any
1874 for (int i = 0; i < len; i++) {
1875 int minutes = reminderMinutes.get(i);
1878 values.put(Reminders.MINUTES, minutes);
1879 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1880 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1881 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1887 private void addRecurrenceRule(ContentValues values) {
1888 updateRecurrenceRule();
1890 if (mRrule == null) {
1894 values.put(Events.RRULE, mRrule);
1895 long end = mEndTime.toMillis(true /* ignore dst */);
1896 long start = mStartTime.toMillis(true /* ignore dst */);
1899 boolean isAllDay = mAllDayCheckBox.isChecked();
1901 long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
1902 duration = "P" + days + "D";
1904 long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
1905 duration = "P" + seconds + "S";
1907 values.put(Events.DURATION, duration);
1910 private void clearRecurrence() {
1911 mEventRecurrence.byday = null;
1912 mEventRecurrence.bydayNum = null;
1913 mEventRecurrence.bydayCount = 0;
1914 mEventRecurrence.bymonth = null;
1915 mEventRecurrence.bymonthCount = 0;
1916 mEventRecurrence.bymonthday = null;
1917 mEventRecurrence.bymonthdayCount = 0;
1920 private void updateRecurrenceRule() {
1921 int position = mRepeatsSpinner.getSelectedItemPosition();
1922 int selection = mRecurrenceIndexes.get(position);
1923 // Make sure we don't have any leftover data from the previous setting
1926 if (selection == DOES_NOT_REPEAT) {
1929 } else if (selection == REPEATS_CUSTOM) {
1930 // Keep custom recurrence as before.
1932 } else if (selection == REPEATS_DAILY) {
1933 mEventRecurrence.freq = EventRecurrence.DAILY;
1934 } else if (selection == REPEATS_EVERY_WEEKDAY) {
1935 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1937 int[] byday = new int[dayCount];
1938 int[] bydayNum = new int[dayCount];
1940 byday[0] = EventRecurrence.MO;
1941 byday[1] = EventRecurrence.TU;
1942 byday[2] = EventRecurrence.WE;
1943 byday[3] = EventRecurrence.TH;
1944 byday[4] = EventRecurrence.FR;
1945 for (int day = 0; day < dayCount; day++) {
1949 mEventRecurrence.byday = byday;
1950 mEventRecurrence.bydayNum = bydayNum;
1951 mEventRecurrence.bydayCount = dayCount;
1952 } else if (selection == REPEATS_WEEKLY_ON_DAY) {
1953 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1954 int[] days = new int[1];
1956 int[] dayNum = new int[dayCount];
1958 days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1959 // not sure why this needs to be zero, but set it for now.
1962 mEventRecurrence.byday = days;
1963 mEventRecurrence.bydayNum = dayNum;
1964 mEventRecurrence.bydayCount = dayCount;
1965 } else if (selection == REPEATS_MONTHLY_ON_DAY) {
1966 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1967 mEventRecurrence.bydayCount = 0;
1968 mEventRecurrence.bymonthdayCount = 1;
1969 int[] bymonthday = new int[1];
1970 bymonthday[0] = mStartTime.monthDay;
1971 mEventRecurrence.bymonthday = bymonthday;
1972 } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
1973 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1974 mEventRecurrence.bydayCount = 1;
1975 mEventRecurrence.bymonthdayCount = 0;
1977 int[] byday = new int[1];
1978 int[] bydayNum = new int[1];
1979 // Compute the week number (for example, the "2nd" Monday)
1980 int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
1981 if (dayCount == 5) {
1984 bydayNum[0] = dayCount;
1985 byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1986 mEventRecurrence.byday = byday;
1987 mEventRecurrence.bydayNum = bydayNum;
1988 } else if (selection == REPEATS_YEARLY) {
1989 mEventRecurrence.freq = EventRecurrence.YEARLY;
1992 // Set the week start day.
1993 mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
1994 mRrule = mEventRecurrence.toString();
1997 private ContentValues getContentValuesFromUi() {
1998 String title = mTitleTextView.getText().toString().trim();
1999 boolean isAllDay = mAllDayCheckBox.isChecked();
2000 String location = mLocationTextView.getText().toString().trim();
2001 String description = mDescriptionTextView.getText().toString().trim();
2003 ContentValues values = new ContentValues();
2005 String timezone = null;
2010 // Reset start and end time, increment the monthDay by 1, and set
2011 // the timezone to UTC, as required for all-day events.
2012 timezone = Time.TIMEZONE_UTC;
2013 mStartTime.hour = 0;
2014 mStartTime.minute = 0;
2015 mStartTime.second = 0;
2016 mStartTime.timezone = timezone;
2017 startMillis = mStartTime.normalize(true);
2020 mEndTime.minute = 0;
2021 mEndTime.second = 0;
2022 mEndTime.monthDay++;
2023 mEndTime.timezone = timezone;
2024 endMillis = mEndTime.normalize(true);
2026 if (mEventCursor == null) {
2027 // This is a new event
2028 calendarId = mCalendarsSpinner.getSelectedItemId();
2030 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
2033 startMillis = mStartTime.toMillis(true);
2034 endMillis = mEndTime.toMillis(true);
2035 if (mEventCursor != null) {
2036 // This is an existing event
2037 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
2039 // The timezone might be null if we are changing an existing
2040 // all-day event to a non-all-day event. We need to assign
2041 // a timezone to the non-all-day event.
2042 if (TextUtils.isEmpty(timezone)) {
2043 timezone = TimeZone.getDefault().getID();
2045 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
2047 // This is a new event
2048 calendarId = mCalendarsSpinner.getSelectedItemId();
2050 // The timezone for a new event is the currently displayed
2051 // timezone, NOT the timezone of the containing calendar.
2052 timezone = TimeZone.getDefault().getID();
2056 values.put(Events.CALENDAR_ID, calendarId);
2057 values.put(Events.EVENT_TIMEZONE, timezone);
2058 values.put(Events.TITLE, title);
2059 values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
2060 values.put(Events.DTSTART, startMillis);
2061 values.put(Events.DTEND, endMillis);
2062 values.put(Events.DESCRIPTION, description);
2063 values.put(Events.EVENT_LOCATION, location);
2064 values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
2066 int visibility = mVisibilitySpinner.getSelectedItemPosition();
2067 if (visibility > 0) {
2068 // For now we the array contains the values 0, 2, and 3. We add one to match.
2071 values.put(Events.VISIBILITY, visibility);
2076 private boolean isEmpty() {
2077 String title = mTitleTextView.getText().toString().trim();
2078 if (title.length() > 0) {
2082 String location = mLocationTextView.getText().toString().trim();
2083 if (location.length() > 0) {
2087 String description = mDescriptionTextView.getText().toString().trim();
2088 if (description.length() > 0) {
2095 private boolean isCustomRecurrence() {
2097 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
2101 if (mEventRecurrence.freq == 0) {
2105 switch (mEventRecurrence.freq) {
2106 case EventRecurrence.DAILY:
2108 case EventRecurrence.WEEKLY:
2109 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
2111 } else if (mEventRecurrence.bydayCount == 1) {
2115 case EventRecurrence.MONTHLY:
2116 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
2118 } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
2122 case EventRecurrence.YEARLY:
2129 private boolean isWeekdayEvent() {
2130 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {