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 // This is to keep track of whether or not multiple calendars have the same display name
257 private static HashMap<String,Boolean> mIsDuplicateName = new HashMap<String,Boolean>();
259 private Time mStartTime;
260 private Time mEndTime;
261 private int mModification = MODIFY_UNINITIALIZED;
262 private int mDefaultReminderMinutes;
264 private DeleteEventHelper mDeleteEventHelper;
265 private QueryHandler mQueryHandler;
267 /* This class is used to update the time buttons. */
268 private class TimeListener implements OnTimeSetListener {
271 public TimeListener(View view) {
275 public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
276 // Cache the member variables locally to avoid inner class overhead.
277 Time startTime = mStartTime;
278 Time endTime = mEndTime;
280 // Cache the start and end millis so that we limit the number
281 // of calls to normalize() and toMillis(), which are fairly
285 if (mView == mStartTimeButton) {
286 // The start time was changed.
287 int hourDuration = endTime.hour - startTime.hour;
288 int minuteDuration = endTime.minute - startTime.minute;
290 startTime.hour = hourOfDay;
291 startTime.minute = minute;
292 startMillis = startTime.normalize(true);
294 // Also update the end time to keep the duration constant.
295 endTime.hour = hourOfDay + hourDuration;
296 endTime.minute = minute + minuteDuration;
298 // The end time was changed.
299 startMillis = startTime.toMillis(true);
300 endTime.hour = hourOfDay;
301 endTime.minute = minute;
303 // Move to the next day if the end time is before the start time.
304 if (endTime.before(startTime)) {
305 endTime.monthDay = startTime.monthDay + 1;
309 endMillis = endTime.normalize(true);
311 setDate(mEndDateButton, endMillis);
312 setTime(mStartTimeButton, startMillis);
313 setTime(mEndTimeButton, endMillis);
317 private class TimeClickListener implements View.OnClickListener {
320 public TimeClickListener(Time time) {
324 public void onClick(View v) {
325 new TimePickerDialog(EditEvent.this, new TimeListener(v),
326 mTime.hour, mTime.minute,
327 DateFormat.is24HourFormat(EditEvent.this)).show();
331 private class DateListener implements OnDateSetListener {
334 public DateListener(View view) {
338 public void onDateSet(DatePicker view, int year, int month, int monthDay) {
339 // Cache the member variables locally to avoid inner class overhead.
340 Time startTime = mStartTime;
341 Time endTime = mEndTime;
343 // Cache the start and end millis so that we limit the number
344 // of calls to normalize() and toMillis(), which are fairly
348 if (mView == mStartDateButton) {
349 // The start date was changed.
350 int yearDuration = endTime.year - startTime.year;
351 int monthDuration = endTime.month - startTime.month;
352 int monthDayDuration = endTime.monthDay - startTime.monthDay;
354 startTime.year = year;
355 startTime.month = month;
356 startTime.monthDay = monthDay;
357 startMillis = startTime.normalize(true);
359 // Also update the end date to keep the duration constant.
360 endTime.year = year + yearDuration;
361 endTime.month = month + monthDuration;
362 endTime.monthDay = monthDay + monthDayDuration;
363 endMillis = endTime.normalize(true);
365 // If the start date has changed then update the repeats.
368 // The end date was changed.
369 startMillis = startTime.toMillis(true);
371 endTime.month = month;
372 endTime.monthDay = monthDay;
373 endMillis = endTime.normalize(true);
375 // Do not allow an event to have an end time before the start time.
376 if (endTime.before(startTime)) {
377 endTime.set(startTime);
378 endMillis = startMillis;
382 setDate(mStartDateButton, startMillis);
383 setDate(mEndDateButton, endMillis);
384 setTime(mEndTimeButton, endMillis); // In case end time had to be reset
388 private class DateClickListener implements View.OnClickListener {
391 public DateClickListener(Time time) {
395 public void onClick(View v) {
396 new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year,
397 mTime.month, mTime.monthDay).show();
401 static private class CalendarsAdapter extends ResourceCursorAdapter {
402 public CalendarsAdapter(Context context, Cursor c) {
403 super(context, R.layout.calendars_item, c);
404 setDropDownViewResource(R.layout.calendars_dropdown_item);
408 public void bindView(View view, Context context, Cursor cursor) {
409 View colorBar = view.findViewById(R.id.color);
410 if (colorBar != null) {
411 colorBar.setBackgroundDrawable(
412 Utils.getColorChip(cursor.getInt(CALENDARS_INDEX_COLOR)));
415 TextView name = (TextView) view.findViewById(R.id.calendar_name);
417 String displayName = cursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
418 name.setText(displayName);
419 name.setTextColor(0xFF000000);
421 TextView accountName = (TextView) view.findViewById(R.id.account_name);
422 if(accountName != null) {
423 if (mIsDuplicateName.containsKey(displayName)
424 && mIsDuplicateName.get(displayName)) {
425 accountName.setText(cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT));
426 accountName.setVisibility(TextView.VISIBLE);
428 accountName.setVisibility(TextView.GONE);
435 // This is called if the user clicks on one of the buttons: "Save",
436 // "Discard", or "Delete". This is also called if the user clicks
437 // on the "remove reminder" button.
438 public void onClick(View v) {
439 if (v == mSaveButton) {
446 if (v == mDeleteButton) {
447 long begin = mStartTime.toMillis(false /* use isDst */);
448 long end = mEndTime.toMillis(false /* use isDst */);
450 switch (mModification) {
451 case MODIFY_SELECTED:
452 which = DeleteEventHelper.DELETE_SELECTED;
454 case MODIFY_ALL_FOLLOWING:
455 which = DeleteEventHelper.DELETE_ALL_FOLLOWING;
458 which = DeleteEventHelper.DELETE_ALL;
461 mDeleteEventHelper.delete(begin, end, mEventCursor, which);
465 if (v == mDiscardButton) {
470 // This must be a click on one of the "remove reminder" buttons
471 LinearLayout reminderItem = (LinearLayout) v.getParent();
472 LinearLayout parent = (LinearLayout) reminderItem.getParent();
473 parent.removeView(reminderItem);
474 mReminderItems.remove(reminderItem);
475 updateRemindersVisibility();
478 // This is called if the user cancels a popup dialog. There are two
479 // dialogs: the "Loading calendars" dialog, and the "No calendars"
480 // dialog. The "Loading calendars" dialog is shown if there is a delay
481 // in loading the calendars (needed when creating an event) and the user
482 // tries to save the event before the calendars have finished loading.
483 // The "No calendars" dialog is shown if there are no syncable calendars.
484 public void onCancel(DialogInterface dialog) {
485 if (dialog == mLoadingCalendarsDialog) {
486 mSaveAfterQueryComplete = false;
487 } else if (dialog == mNoCalendarsDialog) {
492 // This is called if the user clicks on a dialog button.
493 public void onClick(DialogInterface dialog, int which) {
494 if (dialog == mNoCalendarsDialog) {
499 private class QueryHandler extends AsyncQueryHandler {
500 public QueryHandler(ContentResolver cr) {
505 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
506 // If the query didn't return a cursor for some reason return
507 if (cursor == null) {
511 // If the Activity is finishing, then close the cursor.
512 // Otherwise, use the new cursor in the adapter.
514 stopManagingCursor(cursor);
517 mCalendarsCursor = cursor;
518 startManagingCursor(cursor);
521 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
522 Window.PROGRESS_VISIBILITY_OFF);
524 // If there are no syncable calendars, then we cannot allow
525 // creating a new event.
526 if (cursor.getCount() == 0) {
527 // Cancel the "loading calendars" dialog if it exists
528 if (mSaveAfterQueryComplete) {
529 mLoadingCalendarsDialog.cancel();
532 // Create an error message for the user that, when clicked,
533 // will exit this activity without saving the event.
534 AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
535 builder.setTitle(R.string.no_syncable_calendars)
536 .setIcon(android.R.drawable.ic_dialog_alert)
537 .setMessage(R.string.no_calendars_found)
538 .setPositiveButton(android.R.string.ok, EditEvent.this)
539 .setOnCancelListener(EditEvent.this);
540 mNoCalendarsDialog = builder.show();
544 Utils.checkForDuplicateNames(mIsDuplicateName, cursor,
545 CALENDARS_INDEX_DISPLAY_NAME);
547 int defaultCalendarPosition = findDefaultCalendarPosition(mCalendarsCursor);
549 // populate the calendars spinner
550 CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor);
551 mCalendarsSpinner.setAdapter(adapter);
552 mCalendarsSpinner.setSelection(defaultCalendarPosition);
553 mCalendarsQueryComplete = true;
554 if (mSaveAfterQueryComplete) {
555 mLoadingCalendarsDialog.cancel();
560 // Find user domain and set it to the validator.
561 // TODO: we may want to update this validator if the user actually picks
562 // a different calendar. maybe not. depends on what we want for the
563 // user experience. this may change when we add support for multiple
565 if (mHasAttendeeData && cursor.moveToPosition(defaultCalendarPosition)) {
566 String ownEmail = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
567 if (ownEmail != null) {
568 String domain = extractDomain(ownEmail);
569 if (domain != null) {
570 mEmailValidator = new Rfc822Validator(domain);
571 mAttendeesList.setValidator(mEmailValidator);
578 // Find the calendar position in the cursor that matches calendar in preference
579 private int findDefaultCalendarPosition(Cursor calendarsCursor) {
580 if (calendarsCursor.getCount() <= 0) {
584 String defaultCalendar = Utils.getSharedPreference(EditEvent.this,
585 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, null);
587 if (defaultCalendar == null) {
592 calendarsCursor.moveToPosition(-1);
593 while(calendarsCursor.moveToNext()) {
594 if (defaultCalendar.equals(mCalendarsCursor
595 .getString(CALENDARS_INDEX_OWNER_ACCOUNT))) {
604 private static String extractDomain(String email) {
605 int separator = email.lastIndexOf('@');
606 if (separator != -1 && ++separator < email.length()) {
607 return email.substring(separator);
613 protected void onCreate(Bundle icicle) {
614 super.onCreate(icicle);
615 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
616 setContentView(R.layout.edit_event);
618 boolean newEvent = false;
620 mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
622 mStartTime = new Time();
623 mEndTime = new Time();
625 Intent intent = getIntent();
626 mUri = intent.getData();
629 mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null, null);
630 if (mEventCursor == null || mEventCursor.getCount() == 0) {
631 // The cursor is empty. This can happen if the event was deleted.
637 long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
638 long end = intent.getLongExtra(EVENT_END_TIME, 0);
640 String domain = "gmail.com";
642 boolean allDay = false;
643 if (mEventCursor != null) {
644 // The event already exists so fetch the all-day status
645 mEventCursor.moveToFirst();
646 mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
647 allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
648 String rrule = mEventCursor.getString(EVENT_INDEX_RRULE);
649 String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
650 long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
651 mOwnerAccount = mEventCursor.getString(EVENT_INDEX_OWNER_ACCOUNT);
652 if (!TextUtils.isEmpty(mOwnerAccount)) {
653 String ownerDomain = extractDomain(mOwnerAccount);
654 if (ownerDomain != null) {
655 domain = ownerDomain;
659 // Remember the initial values
660 mInitialValues = new ContentValues();
661 mInitialValues.put(EVENT_BEGIN_TIME, begin);
662 mInitialValues.put(EVENT_END_TIME, end);
663 mInitialValues.put(Events.ALL_DAY, allDay ? 1 : 0);
664 mInitialValues.put(Events.RRULE, rrule);
665 mInitialValues.put(Events.EVENT_TIMEZONE, timezone);
666 mInitialValues.put(Events.CALENDAR_ID, calendarId);
669 // We are creating a new event, so set the default from the
670 // intent (if specified).
671 allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false);
674 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
675 Window.PROGRESS_VISIBILITY_ON);
677 // Start a query in the background to read the list of calendars
678 mQueryHandler = new QueryHandler(getContentResolver());
679 mQueryHandler.startQuery(0, null, Calendars.CONTENT_URI, CALENDARS_PROJECTION,
680 CALENDARS_WHERE, null /* selection args */, null /* sort order */);
683 // If the event is all-day, read the times in UTC timezone
686 String tz = mStartTime.timezone;
687 mStartTime.timezone = Time.TIMEZONE_UTC;
688 mStartTime.set(begin);
689 mStartTime.timezone = tz;
691 // Calling normalize to calculate isDst
692 mStartTime.normalize(true);
694 mStartTime.set(begin);
700 String tz = mStartTime.timezone;
701 mEndTime.timezone = Time.TIMEZONE_UTC;
703 mEndTime.timezone = tz;
705 // Calling normalize to calculate isDst
706 mEndTime.normalize(true);
712 // cache all the widgets
713 mTitleTextView = (TextView) findViewById(R.id.title);
714 mLocationTextView = (TextView) findViewById(R.id.location);
715 mDescriptionTextView = (TextView) findViewById(R.id.description);
716 mStartDateButton = (Button) findViewById(R.id.start_date);
717 mEndDateButton = (Button) findViewById(R.id.end_date);
718 mStartTimeButton = (Button) findViewById(R.id.start_time);
719 mEndTimeButton = (Button) findViewById(R.id.end_time);
720 mAllDayCheckBox = (CheckBox) findViewById(R.id.is_all_day);
721 mCalendarsSpinner = (Spinner) findViewById(R.id.calendars);
722 mRepeatsSpinner = (Spinner) findViewById(R.id.repeats);
723 mAvailabilitySpinner = (Spinner) findViewById(R.id.availability);
724 mVisibilitySpinner = (Spinner) findViewById(R.id.visibility);
725 mRemindersSeparator = findViewById(R.id.reminders_separator);
726 mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
727 mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
729 if (mHasAttendeeData) {
730 mAddressAdapter = new EmailAddressAdapter(this);
731 mEmailValidator = new Rfc822Validator(domain);
732 mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees);
734 findViewById(R.id.attendees_group).setVisibility(View.GONE);
737 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
738 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
740 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
742 long endMillis = mEndTime.normalize(true);
744 // Do not allow an event to have an end time before the start time.
745 if (mEndTime.before(mStartTime)) {
746 mEndTime.set(mStartTime);
747 endMillis = mEndTime.normalize(true);
749 setDate(mEndDateButton, endMillis);
750 setTime(mEndTimeButton, endMillis);
753 mStartTimeButton.setVisibility(View.GONE);
754 mEndTimeButton.setVisibility(View.GONE);
756 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
758 long endMillis = mEndTime.normalize(true);
759 setDate(mEndDateButton, endMillis);
760 setTime(mEndTimeButton, endMillis);
763 mStartTimeButton.setVisibility(View.VISIBLE);
764 mEndTimeButton.setVisibility(View.VISIBLE);
770 mAllDayCheckBox.setChecked(true);
772 mAllDayCheckBox.setChecked(false);
775 mSaveButton = (Button) findViewById(R.id.save);
776 mSaveButton.setOnClickListener(this);
778 mDeleteButton = (Button) findViewById(R.id.delete);
779 mDeleteButton.setOnClickListener(this);
781 mDiscardButton = (Button) findViewById(R.id.discard);
782 mDiscardButton.setOnClickListener(this);
784 // Initialize the reminder values array.
785 Resources r = getResources();
786 String[] strings = r.getStringArray(R.array.reminder_minutes_values);
787 int size = strings.length;
788 ArrayList<Integer> list = new ArrayList<Integer>(size);
789 for (int i = 0 ; i < size ; i++) {
790 list.add(Integer.parseInt(strings[i]));
792 mReminderValues = list;
793 String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
794 mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
796 SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(this);
797 String durationString =
798 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
799 mDefaultReminderMinutes = Integer.parseInt(durationString);
801 if (newEvent && mDefaultReminderMinutes != 0) {
802 addReminder(this, this, mReminderItems, mReminderValues,
803 mReminderLabels, mDefaultReminderMinutes);
806 long eventId = (mEventCursor == null) ? -1 : mEventCursor.getLong(EVENT_INDEX_ID);
807 ContentResolver cr = getContentResolver();
810 boolean hasAlarm = (mEventCursor != null)
811 && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0);
813 Uri uri = Reminders.CONTENT_URI;
814 String where = String.format(REMINDERS_WHERE, eventId);
815 Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
817 // First pass: collect all the custom reminder minutes (e.g.,
818 // a reminder of 8 minutes) into a global list.
819 while (reminderCursor.moveToNext()) {
820 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
821 EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
824 // Second pass: create the reminder spinners
825 reminderCursor.moveToPosition(-1);
826 while (reminderCursor.moveToNext()) {
827 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
828 mOriginalMinutes.add(minutes);
829 EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
830 mReminderLabels, minutes);
833 reminderCursor.close();
836 updateRemindersVisibility();
838 // Setup the + Add Reminder Button
839 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
840 public void onClick(View v) {
844 ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
845 reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
847 mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
850 if (mHasAttendeeData && eventId != -1) {
851 Uri uri = Attendees.CONTENT_URI;
852 String[] whereArgs = {Long.toString(eventId)};
853 Cursor attendeeCursor = cr.query(uri, ATTENDEES_PROJECTION, ATTENDEES_WHERE, whereArgs,
856 StringBuilder b = new StringBuilder();
857 while (attendeeCursor.moveToNext()) {
858 String name = attendeeCursor.getString(ATTENDEES_INDEX_NAME);
859 String email = attendeeCursor.getString(ATTENDEES_INDEX_EMAIL);
861 if (name != null && name.length() > 0 && !name.equals(email)) {
862 b.append('"').append(name).append("\" ");
864 b.append('<').append(email).append(">, ");
867 if (b.length() > 0) {
868 mOriginalAttendees = b.toString();
869 mAttendeesList.setText(mOriginalAttendees);
872 attendeeCursor.close();
875 if (mEventCursor == null) {
876 // Allow the intent to specify the fields in the event.
877 // This will allow other apps to create events easily.
878 initFromIntent(intent);
882 private LinkedHashSet<Rfc822Token> getAddressesFromList(MultiAutoCompleteTextView list) {
883 list.clearComposingText();
884 LinkedHashSet<Rfc822Token> addresses = new LinkedHashSet<Rfc822Token>();
885 Rfc822Tokenizer.tokenize(list.getText(), addresses);
887 // validate the emails, out of paranoia. they should already be
888 // validated on input, but drop any invalid emails just to be safe.
889 Iterator<Rfc822Token> addressIterator = addresses.iterator();
890 while (addressIterator.hasNext()) {
891 Rfc822Token address = addressIterator.next();
892 if (!mEmailValidator.isValid(address.getAddress())) {
893 Log.w(TAG, "Dropping invalid attendee email address: " + address);
894 addressIterator.remove();
900 // From com.google.android.gm.ComposeActivity
901 private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) {
902 MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res);
903 list.setAdapter(mAddressAdapter);
904 list.setTokenizer(new Rfc822Tokenizer());
905 list.setValidator(mEmailValidator);
907 // NOTE: assumes no other filters are set
908 list.setFilters(sRecipientFilters);
914 * From com.google.android.gm.ComposeActivity
915 * Implements special address cleanup rules:
916 * The first space key entry following an "@" symbol that is followed by any combination
917 * of letters and symbols, including one+ dots and zero commas, should insert an extra
918 * comma (followed by the space).
920 private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() };
922 private void initFromIntent(Intent intent) {
923 String title = intent.getStringExtra(Events.TITLE);
925 mTitleTextView.setText(title);
928 String location = intent.getStringExtra(Events.EVENT_LOCATION);
929 if (location != null) {
930 mLocationTextView.setText(location);
933 String description = intent.getStringExtra(Events.DESCRIPTION);
934 if (description != null) {
935 mDescriptionTextView.setText(description);
938 int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
939 if (availability != -1) {
940 mAvailabilitySpinner.setSelection(availability);
943 int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
944 if (visibility != -1) {
945 mVisibilitySpinner.setSelection(visibility);
948 String rrule = intent.getStringExtra(Events.RRULE);
951 mEventRecurrence.parse(rrule);
956 protected void onResume() {
960 if (mEventCursor == null || mEventCursor.getCount() == 0) {
961 // The cursor is empty. This can happen if the event was deleted.
967 if (mEventCursor != null) {
968 Cursor cursor = mEventCursor;
969 cursor.moveToFirst();
971 mRrule = cursor.getString(EVENT_INDEX_RRULE);
972 String title = cursor.getString(EVENT_INDEX_TITLE);
973 String description = cursor.getString(EVENT_INDEX_DESCRIPTION);
974 String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
975 int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY);
976 int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY);
977 if (visibility > 0) {
978 // For now we the array contains the values 0, 2, and 3. We subtract one to match.
982 if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) {
983 // If this event has not been synced, then don't allow deleting
984 // or changing a single instance.
985 mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
986 mEventRecurrence.parse(mRrule);
988 // If we haven't synced this repeating event yet, then don't
989 // allow the user to change just one instance.
991 CharSequence[] items;
992 if (mSyncId == null) {
993 if(isFirstEventInSeries()) {
994 // Still display the option so the user knows all events are changing
995 items = new CharSequence[1];
997 items = new CharSequence[2];
1000 if(isFirstEventInSeries()) {
1001 items = new CharSequence[2];
1003 items = new CharSequence[3];
1005 items[itemIndex++] = getText(R.string.modify_event);
1007 items[itemIndex++] = getText(R.string.modify_all);
1009 // Do one more check to make sure this remains at the end of the list
1010 if(!isFirstEventInSeries()) {
1011 // TODO Find out why modify all following causes a dup of the first event if
1012 // it's operating on the first event.
1013 items[itemIndex++] = getText(R.string.modify_all_following);
1016 // Display the modification dialog.
1017 new AlertDialog.Builder(this)
1018 .setOnCancelListener(new OnCancelListener() {
1019 public void onCancel(DialogInterface dialog) {
1023 .setTitle(R.string.edit_event_label)
1024 .setItems(items, new OnClickListener() {
1025 public void onClick(DialogInterface dialog, int which) {
1028 (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED;
1029 } else if (which == 1) {
1031 (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL;
1032 } else if (which == 2) {
1033 mModification = MODIFY_ALL_FOLLOWING;
1036 // If we are modifying all the events in a
1037 // series then disable and ignore the date.
1038 if (mModification == MODIFY_ALL) {
1039 mStartDateButton.setEnabled(false);
1040 mEndDateButton.setEnabled(false);
1041 } else if (mModification == MODIFY_SELECTED) {
1042 mRepeatsSpinner.setEnabled(false);
1049 mTitleTextView.setText(title);
1050 mLocationTextView.setText(location);
1051 mDescriptionTextView.setText(description);
1052 mAvailabilitySpinner.setSelection(availability);
1053 mVisibilitySpinner.setSelection(visibility);
1055 // This is an existing event so hide the calendar spinner
1056 // since we can't change the calendar.
1057 View calendarGroup = findViewById(R.id.calendar_group);
1058 calendarGroup.setVisibility(View.GONE);
1061 if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) {
1062 mStartTime.setToNow();
1064 // Round the time to the nearest half hour.
1065 mStartTime.second = 0;
1066 int minute = mStartTime.minute;
1068 // We are already on a half hour increment
1069 } else if (minute > 0 && minute <= 30) {
1070 mStartTime.minute = 30;
1072 mStartTime.minute = 0;
1073 mStartTime.hour += 1;
1076 long startMillis = mStartTime.normalize(true /* ignore isDst */);
1077 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
1080 // Hide delete button
1081 mDeleteButton.setVisibility(View.GONE);
1084 updateRemindersVisibility();
1090 public boolean onCreateOptionsMenu(Menu menu) {
1092 item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
1093 R.string.add_new_reminder);
1094 item.setIcon(R.drawable.ic_menu_reminder);
1095 item.setAlphabeticShortcut('r');
1097 item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
1098 R.string.edit_event_show_extra_options);
1099 item.setIcon(R.drawable.ic_menu_show_list);
1100 item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
1101 R.string.edit_event_hide_extra_options);
1102 item.setIcon(R.drawable.ic_menu_show_list);
1104 return super.onCreateOptionsMenu(menu);
1108 public boolean onPrepareOptionsMenu(Menu menu) {
1109 if (mReminderItems.size() < MAX_REMINDERS) {
1110 menu.setGroupVisible(MENU_GROUP_REMINDER, true);
1111 menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
1113 menu.setGroupVisible(MENU_GROUP_REMINDER, false);
1114 menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
1117 if (mExtraOptions.getVisibility() == View.VISIBLE) {
1118 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
1119 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
1121 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
1122 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
1125 return super.onPrepareOptionsMenu(menu);
1128 private void addReminder() {
1129 // TODO: when adding a new reminder, make it different from the
1130 // last one in the list (if any).
1131 if (mDefaultReminderMinutes == 0) {
1132 addReminder(this, this, mReminderItems, mReminderValues,
1133 mReminderLabels, 10 /* minutes */);
1135 addReminder(this, this, mReminderItems, mReminderValues,
1136 mReminderLabels, mDefaultReminderMinutes);
1138 updateRemindersVisibility();
1142 public boolean onOptionsItemSelected(MenuItem item) {
1143 switch (item.getItemId()) {
1144 case MENU_ADD_REMINDER:
1147 case MENU_SHOW_EXTRA_OPTIONS:
1148 mExtraOptions.setVisibility(View.VISIBLE);
1150 case MENU_HIDE_EXTRA_OPTIONS:
1151 mExtraOptions.setVisibility(View.GONE);
1154 return super.onOptionsItemSelected(item);
1158 public void onBackPressed() {
1159 // If we are creating a new event, do not create it if the
1160 // title, location and description are all empty, in order to
1161 // prevent accidental "no subject" event creations.
1162 if (mUri != null || !isEmpty()) {
1164 // We cannot exit this activity because the calendars
1165 // are still loading.
1172 private void populateWhen() {
1173 long startMillis = mStartTime.toMillis(false /* use isDst */);
1174 long endMillis = mEndTime.toMillis(false /* use isDst */);
1175 setDate(mStartDateButton, startMillis);
1176 setDate(mEndDateButton, endMillis);
1178 setTime(mStartTimeButton, startMillis);
1179 setTime(mEndTimeButton, endMillis);
1181 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
1182 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
1184 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
1185 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
1188 private void populateRepeats() {
1189 Time time = mStartTime;
1190 Resources r = getResources();
1191 int resource = android.R.layout.simple_spinner_item;
1193 String[] days = new String[] {
1194 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
1195 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
1196 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
1197 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
1198 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
1199 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
1200 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
1202 String[] ordinals = r.getStringArray(R.array.ordinal_labels);
1204 // Only display "Custom" in the spinner if the device does not support the
1205 // recurrence functionality of the event. Only display every weekday if
1206 // the event starts on a weekday.
1207 boolean isCustomRecurrence = isCustomRecurrence();
1208 boolean isWeekdayEvent = isWeekdayEvent();
1210 ArrayList<String> repeatArray = new ArrayList<String>(0);
1211 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
1213 repeatArray.add(r.getString(R.string.does_not_repeat));
1214 recurrenceIndexes.add(DOES_NOT_REPEAT);
1216 repeatArray.add(r.getString(R.string.daily));
1217 recurrenceIndexes.add(REPEATS_DAILY);
1219 if (isWeekdayEvent) {
1220 repeatArray.add(r.getString(R.string.every_weekday));
1221 recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
1224 String format = r.getString(R.string.weekly);
1225 repeatArray.add(String.format(format, time.format("%A")));
1226 recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
1228 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
1229 int dayNumber = (time.monthDay - 1) / 7;
1230 format = r.getString(R.string.monthly_on_day_count);
1231 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
1232 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
1234 format = r.getString(R.string.monthly_on_day);
1235 repeatArray.add(String.format(format, time.monthDay));
1236 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
1238 long when = time.toMillis(false);
1239 format = r.getString(R.string.yearly);
1241 if (DateFormat.is24HourFormat(this)) {
1242 flags |= DateUtils.FORMAT_24HOUR;
1244 repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
1245 recurrenceIndexes.add(REPEATS_YEARLY);
1247 if (isCustomRecurrence) {
1248 repeatArray.add(r.getString(R.string.custom));
1249 recurrenceIndexes.add(REPEATS_CUSTOM);
1251 mRecurrenceIndexes = recurrenceIndexes;
1253 int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
1254 if (mRrule != null) {
1255 if (isCustomRecurrence) {
1256 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
1258 switch (mEventRecurrence.freq) {
1259 case EventRecurrence.DAILY:
1260 position = recurrenceIndexes.indexOf(REPEATS_DAILY);
1262 case EventRecurrence.WEEKLY:
1263 if (mEventRecurrence.repeatsOnEveryWeekDay()) {
1264 position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
1266 position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
1269 case EventRecurrence.MONTHLY:
1270 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1271 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
1273 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
1276 case EventRecurrence.YEARLY:
1277 position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
1282 ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
1283 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1284 mRepeatsSpinner.setAdapter(adapter);
1285 mRepeatsSpinner.setSelection(position);
1288 // Adds a reminder to the displayed list of reminders.
1289 // Returns true if successfully added reminder, false if no reminders can
1291 static boolean addReminder(Activity activity, View.OnClickListener listener,
1292 ArrayList<LinearLayout> items, ArrayList<Integer> values,
1293 ArrayList<String> labels, int minutes) {
1295 if (items.size() >= MAX_REMINDERS) {
1299 LayoutInflater inflater = activity.getLayoutInflater();
1300 LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
1301 LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
1302 parent.addView(reminderItem);
1304 Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
1305 Resources res = activity.getResources();
1306 spinner.setPrompt(res.getString(R.string.reminders_label));
1307 int resource = android.R.layout.simple_spinner_item;
1308 ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
1309 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1310 spinner.setAdapter(adapter);
1312 ImageButton reminderRemoveButton;
1313 reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
1314 reminderRemoveButton.setOnClickListener(listener);
1316 int index = findMinutesInReminderList(values, minutes);
1317 spinner.setSelection(index);
1318 items.add(reminderItem);
1323 static void addMinutesToList(Context context, ArrayList<Integer> values,
1324 ArrayList<String> labels, int minutes) {
1325 int index = values.indexOf(minutes);
1330 // The requested "minutes" does not exist in the list, so insert it
1333 String label = constructReminderLabel(context, minutes, false);
1334 int len = values.size();
1335 for (int i = 0; i < len; i++) {
1336 if (minutes < values.get(i)) {
1337 values.add(i, minutes);
1338 labels.add(i, label);
1343 values.add(minutes);
1344 labels.add(len, label);
1348 * Finds the index of the given "minutes" in the "values" list.
1350 * @param values the list of minutes corresponding to the spinner choices
1351 * @param minutes the minutes to search for in the values list
1352 * @return the index of "minutes" in the "values" list
1354 private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
1355 int index = values.indexOf(minutes);
1357 // This should never happen.
1358 Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
1364 // Constructs a label given an arbitrary number of minutes. For example,
1365 // if the given minutes is 63, then this returns the string "63 minutes".
1366 // As another example, if the given minutes is 120, then this returns
1368 static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
1369 Resources resources = context.getResources();
1372 if (minutes % 60 != 0) {
1375 resId = R.plurals.Nmins;
1377 resId = R.plurals.Nminutes;
1379 } else if (minutes % (24 * 60) != 0) {
1380 value = minutes / 60;
1381 resId = R.plurals.Nhours;
1383 value = minutes / ( 24 * 60);
1384 resId = R.plurals.Ndays;
1387 String format = resources.getQuantityString(resId, value);
1388 return String.format(format, value);
1391 private void updateRemindersVisibility() {
1392 if (mReminderItems.size() == 0) {
1393 mRemindersSeparator.setVisibility(View.GONE);
1394 mRemindersContainer.setVisibility(View.GONE);
1396 mRemindersSeparator.setVisibility(View.VISIBLE);
1397 mRemindersContainer.setVisibility(View.VISIBLE);
1401 private void setDate(TextView view, long millis) {
1402 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
1403 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
1404 DateUtils.FORMAT_ABBREV_WEEKDAY;
1405 view.setText(DateUtils.formatDateTime(this, millis, flags));
1408 private void setTime(TextView view, long millis) {
1409 int flags = DateUtils.FORMAT_SHOW_TIME;
1410 if (DateFormat.is24HourFormat(this)) {
1411 flags |= DateUtils.FORMAT_24HOUR;
1413 view.setText(DateUtils.formatDateTime(this, millis, flags));
1416 // Saves the event. Returns true if it is okay to exit this activity.
1417 private boolean save() {
1418 boolean forceSaveReminders = false;
1420 // If we are creating a new event, then make sure we wait until the
1421 // query to fetch the list of calendars has finished.
1422 if (mEventCursor == null) {
1423 if (!mCalendarsQueryComplete) {
1424 // Wait for the calendars query to finish.
1425 if (mLoadingCalendarsDialog == null) {
1426 // Create the progress dialog
1427 mLoadingCalendarsDialog = ProgressDialog.show(this,
1428 getText(R.string.loading_calendars_title),
1429 getText(R.string.loading_calendars_message),
1431 mSaveAfterQueryComplete = true;
1436 // Avoid creating a new event if the calendars cursor is empty or we clicked through
1437 // too quickly and no calendar was selected (blame the monkey)
1438 if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0 ||
1439 mCalendarsSpinner.getSelectedItemId() == AdapterView.INVALID_ROW_ID) {
1440 Log.w("Cal", "The calendars table does not contain any calendars"
1441 + " or no calendar was selected."
1442 + " New event was not created.");
1445 Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
1447 Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
1450 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1451 int eventIdIndex = -1;
1453 ContentValues values = getContentValuesFromUi();
1456 // Update the "hasAlarm" field for the event
1457 ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
1459 int len = reminderMinutes.size();
1460 values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
1462 // For recurring events, we must make sure that we use duration rather
1465 // Add hasAttendeeData for a new event
1466 values.put(Events.HAS_ATTENDEE_DATA, 1);
1467 // Create new event with new contents
1468 addRecurrenceRule(values);
1469 if (mRrule != null) {
1470 values.remove(Events.DTEND);
1472 eventIdIndex = ops.size();
1473 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1475 forceSaveReminders = true;
1477 } else if (mRrule == null) {
1478 // Modify contents of a non-repeating event
1479 addRecurrenceRule(values);
1480 checkTimeDependentFields(values);
1481 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1483 } else if (mInitialValues.getAsString(Events.RRULE) == null) {
1484 // This event was changed from a non-repeating event to a
1486 addRecurrenceRule(values);
1487 values.remove(Events.DTEND);
1488 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1490 } else if (mModification == MODIFY_SELECTED) {
1491 // Modify contents of the current instance of repeating event
1493 // Create a recurrence exception
1494 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1495 values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
1496 values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
1497 boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1498 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
1500 eventIdIndex = ops.size();
1501 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1503 forceSaveReminders = true;
1505 } else if (mModification == MODIFY_ALL_FOLLOWING) {
1506 // Modify this instance and all future instances of repeating event
1507 addRecurrenceRule(values);
1509 if (mRrule == null) {
1510 // We've changed a recurring event to a non-recurring event.
1511 // If the event we are editing is the first in the series,
1512 // then delete the whole series. Otherwise, update the series
1513 // to end at the new start time.
1514 if (isFirstEventInSeries()) {
1515 ops.add(ContentProviderOperation.newDelete(uri).build());
1517 // Update the current repeating event to end at the new
1519 updatePastEvents(ops, uri);
1521 eventIdIndex = ops.size();
1522 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1525 if (isFirstEventInSeries()) {
1526 checkTimeDependentFields(values);
1527 values.remove(Events.DTEND);
1528 Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
1531 // Update the current repeating event to end at the new
1533 updatePastEvents(ops, uri);
1535 // Create a new event with the user-modified fields
1536 values.remove(Events.DTEND);
1537 eventIdIndex = ops.size();
1538 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
1542 forceSaveReminders = true;
1544 } else if (mModification == MODIFY_ALL) {
1546 // Modify all instances of repeating event
1547 addRecurrenceRule(values);
1549 if (mRrule == null) {
1550 // We've changed a recurring event to a non-recurring event.
1551 // Delete the whole series and replace it with a new
1552 // non-recurring event.
1553 ops.add(ContentProviderOperation.newDelete(uri).build());
1555 eventIdIndex = ops.size();
1556 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1558 forceSaveReminders = true;
1560 checkTimeDependentFields(values);
1561 values.remove(Events.DTEND);
1562 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1566 // New Event or New Exception to an existing event
1567 boolean newEvent = (eventIdIndex != -1);
1570 saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
1571 forceSaveReminders);
1572 } else if (uri != null) {
1573 long eventId = ContentUris.parseId(uri);
1574 saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
1575 forceSaveReminders);
1580 // New event/instance - Set Organizer's response as yes
1581 if (mHasAttendeeData && newEvent) {
1583 int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
1585 // Save the default calendar for new events
1586 if (mCalendarsCursor != null) {
1587 if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
1588 String defaultCalendar = mCalendarsCursor
1589 .getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1590 Utils.setSharedPreference(this,
1591 CalendarPreferenceActivity.KEY_DEFAULT_CALENDAR, defaultCalendar);
1595 String ownerEmail = mOwnerAccount;
1596 // Just in case mOwnerAccount is null, try to get owner from mCalendarsCursor
1597 if (ownerEmail == null && mCalendarsCursor != null &&
1598 mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
1599 ownerEmail = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1601 if (ownerEmail != null) {
1602 values.put(Attendees.ATTENDEE_EMAIL, ownerEmail);
1603 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
1604 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1605 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
1607 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1608 .withValues(values);
1609 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1614 // TODO: is this the right test? this currently checks if this is
1615 // a new event or an existing event. or is this a paranoia check?
1616 if (mHasAttendeeData && (newEvent || uri != null)) {
1617 Editable attendeesText = mAttendeesList.getText();
1618 // Hit the content provider only if this is a new event or the user has changed it
1619 if (newEvent || !mOriginalAttendees.equals(attendeesText.toString())) {
1620 // figure out which attendees need to be added and which ones
1621 // need to be deleted. use a linked hash set, so we maintain
1622 // order (but also remove duplicates).
1623 LinkedHashSet<Rfc822Token> newAttendees = getAddressesFromList(mAttendeesList);
1625 // the eventId is only used if eventIdIndex is -1.
1626 // TODO: clean up this code.
1627 long eventId = uri != null ? ContentUris.parseId(uri) : -1;
1629 // only compute deltas if this is an existing event.
1630 // new events (being inserted into the Events table) won't
1631 // have any existing attendees.
1633 HashSet<Rfc822Token> removedAttendees = new HashSet<Rfc822Token>();
1634 HashSet<Rfc822Token> originalAttendees = new HashSet<Rfc822Token>();
1635 Rfc822Tokenizer.tokenize(mOriginalAttendees, originalAttendees);
1636 for (Rfc822Token originalAttendee : originalAttendees) {
1637 if (newAttendees.contains(originalAttendee)) {
1638 // existing attendee. remove from new attendees set.
1639 newAttendees.remove(originalAttendee);
1641 // no longer in attendees. mark as removed.
1642 removedAttendees.add(originalAttendee);
1646 // delete removed attendees
1647 b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
1649 String[] args = new String[removedAttendees.size() + 1];
1650 args[0] = Long.toString(eventId);
1652 StringBuilder deleteWhere = new StringBuilder(ATTENDEES_DELETE_PREFIX);
1653 for (Rfc822Token removedAttendee : removedAttendees) {
1655 deleteWhere.append(",");
1657 deleteWhere.append("?");
1658 args[i++] = removedAttendee.getAddress();
1660 deleteWhere.append(")");
1661 b.withSelection(deleteWhere.toString(), args);
1665 if (newAttendees.size() > 0) {
1666 // Insert the new attendees
1667 for (Rfc822Token attendee : newAttendees) {
1669 values.put(Attendees.ATTENDEE_NAME, attendee.getName());
1670 values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
1671 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
1672 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1673 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
1676 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1677 .withValues(values);
1678 b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
1680 values.put(Attendees.EVENT_ID, eventId);
1681 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1682 .withValues(values);
1691 // TODO Move this to background thread
1692 ContentProviderResult[] results =
1693 getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
1695 for (int i = 0; i < results.length; i++) {
1696 Log.v(TAG, "results = " + results[i].toString());
1699 } catch (RemoteException e) {
1700 Log.w(TAG, "Ignoring unexpected remote exception", e);
1701 } catch (OperationApplicationException e) {
1702 Log.w(TAG, "Ignoring unexpected exception", e);
1708 private boolean isFirstEventInSeries() {
1709 int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
1710 long start = mEventCursor.getLong(dtStart);
1711 return start == mStartTime.toMillis(true);
1714 private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) {
1715 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1716 String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
1717 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1718 String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
1719 mEventRecurrence.parse(oldRrule);
1721 Time untilTime = new Time();
1722 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1723 ContentValues oldValues = new ContentValues();
1725 // The "until" time must be in UTC time in order for Google calendar
1726 // to display it properly. For all-day events, the "until" time string
1727 // must include just the date field, and not the time field. The
1728 // repeating events repeat up to and including the "until" time.
1729 untilTime.timezone = Time.TIMEZONE_UTC;
1731 // Subtract one second from the old begin time to get the new
1733 untilTime.set(begin - 1000); // subtract one second (1000 millis)
1736 untilTime.minute = 0;
1737 untilTime.second = 0;
1738 untilTime.allDay = true;
1739 untilTime.normalize(false);
1741 // For all-day events, the duration must be in days, not seconds.
1742 // Otherwise, Google Calendar will (mistakenly) change this event
1743 // into a non-all-day event.
1744 int len = oldDuration.length();
1745 if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
1746 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
1747 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1748 oldDuration = "P" + days + "D";
1751 mEventRecurrence.until = untilTime.format2445();
1753 oldValues.put(Events.DTSTART, oldStartMillis);
1754 oldValues.put(Events.DURATION, oldDuration);
1755 oldValues.put(Events.RRULE, mEventRecurrence.toString());
1756 Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
1760 private void checkTimeDependentFields(ContentValues values) {
1761 long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1762 long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
1763 boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1764 String oldRrule = mInitialValues.getAsString(Events.RRULE);
1765 String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
1767 long newBegin = values.getAsLong(Events.DTSTART);
1768 long newEnd = values.getAsLong(Events.DTEND);
1769 boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
1770 String newRrule = values.getAsString(Events.RRULE);
1771 String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1773 // If none of the time-dependent fields changed, then remove them.
1774 if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
1775 && TextUtils.equals(oldRrule, newRrule)
1776 && TextUtils.equals(oldTimezone, newTimezone)) {
1777 values.remove(Events.DTSTART);
1778 values.remove(Events.DTEND);
1779 values.remove(Events.DURATION);
1780 values.remove(Events.ALL_DAY);
1781 values.remove(Events.RRULE);
1782 values.remove(Events.EVENT_TIMEZONE);
1786 if (oldRrule == null || newRrule == null) {
1790 // If we are modifying all events then we need to set DTSTART to the
1791 // start time of the first event in the series, not the current
1792 // date and time. If the start time of the event was changed
1793 // (from, say, 3pm to 4pm), then we want to add the time difference
1794 // to the start time of the first event in the series (the DTSTART
1795 // value). If we are modifying one instance or all following instances,
1796 // then we leave the DTSTART field alone.
1797 if (mModification == MODIFY_ALL) {
1798 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1799 if (oldBegin != newBegin) {
1800 // The user changed the start time of this event
1801 long offset = newBegin - oldBegin;
1802 oldStartMillis += offset;
1804 values.put(Events.DTSTART, oldStartMillis);
1808 static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
1809 ArrayList<Integer> reminderValues) {
1810 int len = reminderItems.size();
1811 ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
1812 for (int index = 0; index < len; index++) {
1813 LinearLayout layout = reminderItems.get(index);
1814 Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
1815 int minutes = reminderValues.get(spinner.getSelectedItemPosition());
1816 reminderMinutes.add(minutes);
1818 return reminderMinutes;
1822 * Saves the reminders, if they changed. Returns true if the database
1825 * @param ops the array of ContentProviderOperations
1826 * @param eventId the id of the event whose reminders are being updated
1827 * @param reminderMinutes the array of reminders set by the user
1828 * @param originalMinutes the original array of reminders
1829 * @param forceSave if true, then save the reminders even if they didn't
1831 * @return true if the database was updated
1833 static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
1834 ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
1835 boolean forceSave) {
1836 // If the reminders have not changed, then don't update the database
1837 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1841 // Delete all the existing reminders for this event
1842 String where = Reminders.EVENT_ID + "=?";
1843 String[] args = new String[] { Long.toString(eventId) };
1844 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1845 b.withSelection(where, args);
1848 ContentValues values = new ContentValues();
1849 int len = reminderMinutes.size();
1851 // Insert the new reminders, if any
1852 for (int i = 0; i < len; i++) {
1853 int minutes = reminderMinutes.get(i);
1856 values.put(Reminders.MINUTES, minutes);
1857 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1858 values.put(Reminders.EVENT_ID, eventId);
1859 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1865 static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
1866 int eventIdIndex, ArrayList<Integer> reminderMinutes,
1867 ArrayList<Integer> originalMinutes, boolean forceSave) {
1868 // If the reminders have not changed, then don't update the database
1869 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1873 // Delete all the existing reminders for this event
1874 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1875 b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
1876 b.withSelectionBackReference(0, eventIdIndex);
1879 ContentValues values = new ContentValues();
1880 int len = reminderMinutes.size();
1882 // Insert the new reminders, if any
1883 for (int i = 0; i < len; i++) {
1884 int minutes = reminderMinutes.get(i);
1887 values.put(Reminders.MINUTES, minutes);
1888 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1889 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1890 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1896 private void addRecurrenceRule(ContentValues values) {
1897 updateRecurrenceRule();
1899 if (mRrule == null) {
1903 values.put(Events.RRULE, mRrule);
1904 long end = mEndTime.toMillis(true /* ignore dst */);
1905 long start = mStartTime.toMillis(true /* ignore dst */);
1908 boolean isAllDay = mAllDayCheckBox.isChecked();
1910 long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
1911 duration = "P" + days + "D";
1913 long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
1914 duration = "P" + seconds + "S";
1916 values.put(Events.DURATION, duration);
1919 private void updateRecurrenceRule() {
1920 int position = mRepeatsSpinner.getSelectedItemPosition();
1921 int selection = mRecurrenceIndexes.get(position);
1923 if (selection == DOES_NOT_REPEAT) {
1926 } else if (selection == REPEATS_CUSTOM) {
1927 // Keep custom recurrence as before.
1929 } else if (selection == REPEATS_DAILY) {
1930 mEventRecurrence.freq = EventRecurrence.DAILY;
1931 } else if (selection == REPEATS_EVERY_WEEKDAY) {
1932 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1934 int[] byday = new int[dayCount];
1935 int[] bydayNum = new int[dayCount];
1937 byday[0] = EventRecurrence.MO;
1938 byday[1] = EventRecurrence.TU;
1939 byday[2] = EventRecurrence.WE;
1940 byday[3] = EventRecurrence.TH;
1941 byday[4] = EventRecurrence.FR;
1942 for (int day = 0; day < dayCount; day++) {
1946 mEventRecurrence.byday = byday;
1947 mEventRecurrence.bydayNum = bydayNum;
1948 mEventRecurrence.bydayCount = dayCount;
1949 } else if (selection == REPEATS_WEEKLY_ON_DAY) {
1950 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1951 int[] days = new int[1];
1953 int[] dayNum = new int[dayCount];
1955 days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1956 // not sure why this needs to be zero, but set it for now.
1959 mEventRecurrence.byday = days;
1960 mEventRecurrence.bydayNum = dayNum;
1961 mEventRecurrence.bydayCount = dayCount;
1962 } else if (selection == REPEATS_MONTHLY_ON_DAY) {
1963 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1964 mEventRecurrence.bydayCount = 0;
1965 mEventRecurrence.bymonthdayCount = 1;
1966 int[] bymonthday = new int[1];
1967 bymonthday[0] = mStartTime.monthDay;
1968 mEventRecurrence.bymonthday = bymonthday;
1969 } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
1970 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1971 mEventRecurrence.bydayCount = 1;
1972 mEventRecurrence.bymonthdayCount = 0;
1974 int[] byday = new int[1];
1975 int[] bydayNum = new int[1];
1976 // Compute the week number (for example, the "2nd" Monday)
1977 int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
1978 if (dayCount == 5) {
1981 bydayNum[0] = dayCount;
1982 byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1983 mEventRecurrence.byday = byday;
1984 mEventRecurrence.bydayNum = bydayNum;
1985 } else if (selection == REPEATS_YEARLY) {
1986 mEventRecurrence.freq = EventRecurrence.YEARLY;
1989 // Set the week start day.
1990 mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
1991 mRrule = mEventRecurrence.toString();
1994 private ContentValues getContentValuesFromUi() {
1995 String title = mTitleTextView.getText().toString().trim();
1996 boolean isAllDay = mAllDayCheckBox.isChecked();
1997 String location = mLocationTextView.getText().toString().trim();
1998 String description = mDescriptionTextView.getText().toString().trim();
2000 ContentValues values = new ContentValues();
2002 String timezone = null;
2007 // Reset start and end time, increment the monthDay by 1, and set
2008 // the timezone to UTC, as required for all-day events.
2009 timezone = Time.TIMEZONE_UTC;
2010 mStartTime.hour = 0;
2011 mStartTime.minute = 0;
2012 mStartTime.second = 0;
2013 mStartTime.timezone = timezone;
2014 startMillis = mStartTime.normalize(true);
2017 mEndTime.minute = 0;
2018 mEndTime.second = 0;
2019 mEndTime.monthDay++;
2020 mEndTime.timezone = timezone;
2021 endMillis = mEndTime.normalize(true);
2023 if (mEventCursor == null) {
2024 // This is a new event
2025 calendarId = mCalendarsSpinner.getSelectedItemId();
2027 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
2030 startMillis = mStartTime.toMillis(true);
2031 endMillis = mEndTime.toMillis(true);
2032 if (mEventCursor != null) {
2033 // This is an existing event
2034 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
2036 // The timezone might be null if we are changing an existing
2037 // all-day event to a non-all-day event. We need to assign
2038 // a timezone to the non-all-day event.
2039 if (TextUtils.isEmpty(timezone)) {
2040 timezone = TimeZone.getDefault().getID();
2042 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
2044 // This is a new event
2045 calendarId = mCalendarsSpinner.getSelectedItemId();
2047 // The timezone for a new event is the currently displayed
2048 // timezone, NOT the timezone of the containing calendar.
2049 timezone = TimeZone.getDefault().getID();
2053 values.put(Events.CALENDAR_ID, calendarId);
2054 values.put(Events.EVENT_TIMEZONE, timezone);
2055 values.put(Events.TITLE, title);
2056 values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
2057 values.put(Events.DTSTART, startMillis);
2058 values.put(Events.DTEND, endMillis);
2059 values.put(Events.DESCRIPTION, description);
2060 values.put(Events.EVENT_LOCATION, location);
2061 values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
2063 int visibility = mVisibilitySpinner.getSelectedItemPosition();
2064 if (visibility > 0) {
2065 // For now we the array contains the values 0, 2, and 3. We add one to match.
2068 values.put(Events.VISIBILITY, visibility);
2073 private boolean isEmpty() {
2074 String title = mTitleTextView.getText().toString().trim();
2075 if (title.length() > 0) {
2079 String location = mLocationTextView.getText().toString().trim();
2080 if (location.length() > 0) {
2084 String description = mDescriptionTextView.getText().toString().trim();
2085 if (description.length() > 0) {
2092 private boolean isCustomRecurrence() {
2094 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
2098 if (mEventRecurrence.freq == 0) {
2102 switch (mEventRecurrence.freq) {
2103 case EventRecurrence.DAILY:
2105 case EventRecurrence.WEEKLY:
2106 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
2108 } else if (mEventRecurrence.bydayCount == 1) {
2112 case EventRecurrence.MONTHLY:
2113 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
2115 } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
2119 case EventRecurrence.YEARLY:
2126 private boolean isWeekdayEvent() {
2127 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {