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 android.accounts.Account;
23 import android.accounts.AccountManager;
24 import android.accounts.AuthenticatorException;
25 import android.accounts.OperationCanceledException;
26 import android.app.Activity;
27 import android.app.AlertDialog;
28 import android.app.DatePickerDialog;
29 import android.app.ProgressDialog;
30 import android.app.TimePickerDialog;
31 import android.app.DatePickerDialog.OnDateSetListener;
32 import android.app.TimePickerDialog.OnTimeSetListener;
33 import android.content.AsyncQueryHandler;
34 import android.content.ContentProviderOperation;
35 import android.content.ContentProviderResult;
36 import android.content.ContentResolver;
37 import android.content.ContentUris;
38 import android.content.ContentValues;
39 import android.content.Context;
40 import android.content.DialogInterface;
41 import android.content.Intent;
42 import android.content.OperationApplicationException;
43 import android.content.SharedPreferences;
44 import android.content.ContentProviderOperation.Builder;
45 import android.content.DialogInterface.OnCancelListener;
46 import android.content.DialogInterface.OnClickListener;
47 import android.content.res.Resources;
48 import android.database.Cursor;
49 import android.net.Uri;
50 import android.os.Bundle;
51 import android.os.RemoteException;
52 import android.pim.EventRecurrence;
53 import android.preference.PreferenceManager;
54 import android.provider.Calendar.Attendees;
55 import android.provider.Calendar.Calendars;
56 import android.provider.Calendar.Events;
57 import android.provider.Calendar.Reminders;
58 import android.text.Editable;
59 import android.text.InputFilter;
60 import android.text.TextUtils;
61 import android.text.format.DateFormat;
62 import android.text.format.DateUtils;
63 import android.text.format.Time;
64 import android.text.util.Rfc822InputFilter;
65 import android.text.util.Rfc822Token;
66 import android.text.util.Rfc822Tokenizer;
67 import android.text.util.Rfc822Validator;
68 import android.util.Log;
69 import android.view.LayoutInflater;
70 import android.view.Menu;
71 import android.view.MenuItem;
72 import android.view.View;
73 import android.view.Window;
74 import android.widget.ArrayAdapter;
75 import android.widget.Button;
76 import android.widget.CheckBox;
77 import android.widget.CompoundButton;
78 import android.widget.DatePicker;
79 import android.widget.ImageButton;
80 import android.widget.LinearLayout;
81 import android.widget.MultiAutoCompleteTextView;
82 import android.widget.ResourceCursorAdapter;
83 import android.widget.Spinner;
84 import android.widget.TextView;
85 import android.widget.TimePicker;
86 import android.widget.Toast;
88 import com.google.android.googlelogin.GoogleLoginServiceConstants;
90 import java.io.IOException;
91 import java.util.ArrayList;
92 import java.util.Arrays;
93 import java.util.Calendar;
94 import java.util.HashSet;
95 import java.util.LinkedHashSet;
96 import java.util.TimeZone;
98 public class EditEvent extends Activity implements View.OnClickListener,
99 DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
100 private static final String TAG = "EditEvent";
101 private static final boolean DEBUG = false;
104 * This is the symbolic name for the key used to pass in the boolean
105 * for creating all-day events that is part of the extra data of the intent.
106 * This is used only for creating new events and is set to true if
107 * the default for the new event should be an all-day event.
109 public static final String EVENT_ALL_DAY = "allDay";
111 private static final int MAX_REMINDERS = 5;
113 private static final int MENU_GROUP_REMINDER = 1;
114 private static final int MENU_GROUP_SHOW_OPTIONS = 2;
115 private static final int MENU_GROUP_HIDE_OPTIONS = 3;
117 private static final int MENU_ADD_REMINDER = 1;
118 private static final int MENU_SHOW_EXTRA_OPTIONS = 2;
119 private static final int MENU_HIDE_EXTRA_OPTIONS = 3;
121 private static final String[] EVENT_PROJECTION = new String[] {
124 Events.DESCRIPTION, // 2
125 Events.EVENT_LOCATION, // 3
127 Events.HAS_ALARM, // 5
128 Events.CALENDAR_ID, // 6
130 Events.DURATION, // 8
131 Events.EVENT_TIMEZONE, // 9
133 Events._SYNC_ID, // 11
134 Events.TRANSPARENCY, // 12
135 Events.VISIBILITY, // 13
136 Events.OWNER_ACCOUNT, // 14
137 Events.HAS_ATTENDEE_DATA, // 15
139 private static final int EVENT_INDEX_ID = 0;
140 private static final int EVENT_INDEX_TITLE = 1;
141 private static final int EVENT_INDEX_DESCRIPTION = 2;
142 private static final int EVENT_INDEX_EVENT_LOCATION = 3;
143 private static final int EVENT_INDEX_ALL_DAY = 4;
144 private static final int EVENT_INDEX_HAS_ALARM = 5;
145 private static final int EVENT_INDEX_CALENDAR_ID = 6;
146 private static final int EVENT_INDEX_DTSTART = 7;
147 private static final int EVENT_INDEX_DURATION = 8;
148 private static final int EVENT_INDEX_TIMEZONE = 9;
149 private static final int EVENT_INDEX_RRULE = 10;
150 private static final int EVENT_INDEX_SYNC_ID = 11;
151 private static final int EVENT_INDEX_TRANSPARENCY = 12;
152 private static final int EVENT_INDEX_VISIBILITY = 13;
153 private static final int EVENT_INDEX_OWNER_ACCOUNT = 14;
154 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 15;
156 private static final String[] CALENDARS_PROJECTION = new String[] {
158 Calendars.DISPLAY_NAME, // 1
159 Calendars.OWNER_ACCOUNT, // 2
161 private static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
162 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
163 private static final String CALENDARS_WHERE = Calendars.ACCESS_LEVEL + ">=" +
164 Calendars.CONTRIBUTOR_ACCESS + " AND " + Calendars.SYNC_EVENTS + "=1";
166 private static final String[] REMINDERS_PROJECTION = new String[] {
168 Reminders.MINUTES, // 1
170 private static final int REMINDERS_INDEX_MINUTES = 1;
171 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
172 Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
173 Reminders.METHOD_DEFAULT + ")";
175 private static final String[] ATTENDEES_PROJECTION = new String[] {
176 Attendees.ATTENDEE_NAME, // 0
177 Attendees.ATTENDEE_EMAIL, // 1
179 private static final int ATTENDEES_INDEX_NAME = 0;
180 private static final int ATTENDEES_INDEX_EMAIL = 1;
181 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=? AND "
182 + Attendees.ATTENDEE_RELATIONSHIP + "<>" + Attendees.RELATIONSHIP_ORGANIZER;
183 private static final String ATTENDEES_DELETE_PREFIX = Attendees.EVENT_ID + "=? AND " +
184 Attendees.ATTENDEE_EMAIL + " IN (";
186 private static final int DOES_NOT_REPEAT = 0;
187 private static final int REPEATS_DAILY = 1;
188 private static final int REPEATS_EVERY_WEEKDAY = 2;
189 private static final int REPEATS_WEEKLY_ON_DAY = 3;
190 private static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
191 private static final int REPEATS_MONTHLY_ON_DAY = 5;
192 private static final int REPEATS_YEARLY = 6;
193 private static final int REPEATS_CUSTOM = 7;
195 private static final int MODIFY_UNINITIALIZED = 0;
196 private static final int MODIFY_SELECTED = 1;
197 private static final int MODIFY_ALL = 2;
198 private static final int MODIFY_ALL_FOLLOWING = 3;
200 private static final int DAY_IN_SECONDS = 24 * 60 * 60;
202 private int mFirstDayOfWeek; // cached in onCreate
204 private Cursor mEventCursor;
205 private Cursor mCalendarsCursor;
207 private Button mStartDateButton;
208 private Button mEndDateButton;
209 private Button mStartTimeButton;
210 private Button mEndTimeButton;
211 private Button mSaveButton;
212 private Button mDeleteButton;
213 private Button mDiscardButton;
214 private CheckBox mAllDayCheckBox;
215 private Spinner mCalendarsSpinner;
216 private Spinner mRepeatsSpinner;
217 private Spinner mAvailabilitySpinner;
218 private Spinner mVisibilitySpinner;
219 private TextView mTitleTextView;
220 private TextView mLocationTextView;
221 private TextView mDescriptionTextView;
222 private View mRemindersSeparator;
223 private LinearLayout mRemindersContainer;
224 private LinearLayout mExtraOptions;
225 private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
226 private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
227 private Rfc822Validator mEmailValidator;
228 private MultiAutoCompleteTextView mAttendeesList;
229 private EmailAddressAdapter mAddressAdapter;
230 private String mOriginalAttendees = "";
232 // Used to control the visibility of the Guests textview. Default to true
233 private boolean mHasAttendeeData = true;
235 private EventRecurrence mEventRecurrence = new EventRecurrence();
236 private String mRrule;
237 private boolean mCalendarsQueryComplete;
238 private boolean mSaveAfterQueryComplete;
239 private ProgressDialog mLoadingCalendarsDialog;
240 private AlertDialog mNoCalendarsDialog;
241 private ContentValues mInitialValues;
242 private String mOwnerAccount;
245 * If the repeating event is created on the phone and it hasn't been
246 * synced yet to the web server, then there is a bug where you can't
247 * delete or change an instance of the repeating event. This case
248 * can be detected with mSyncId. If mSyncId == null, then the repeating
249 * event has not been synced to the phone, in which case we won't allow
250 * the user to change one instance.
252 private String mSyncId;
254 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0);
255 private ArrayList<Integer> mReminderValues;
256 private ArrayList<String> mReminderLabels;
258 private Time mStartTime;
259 private Time mEndTime;
260 private int mModification = MODIFY_UNINITIALIZED;
261 private int mDefaultReminderMinutes;
263 private DeleteEventHelper mDeleteEventHelper;
264 private QueryHandler mQueryHandler;
265 private AccountManager mAccountManager;
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;
297 endMillis = endTime.normalize(true);
299 // The end time was changed.
300 startMillis = startTime.toMillis(true);
301 endTime.hour = hourOfDay;
302 endTime.minute = minute;
303 endMillis = endTime.normalize(true);
305 // Do not allow an event to have an end time before the start time.
306 if (endTime.before(startTime)) {
307 endTime.set(startTime);
308 endMillis = startMillis;
312 setDate(mEndDateButton, endMillis);
313 setTime(mStartTimeButton, startMillis);
314 setTime(mEndTimeButton, endMillis);
318 private class TimeClickListener implements View.OnClickListener {
321 public TimeClickListener(Time time) {
325 public void onClick(View v) {
326 new TimePickerDialog(EditEvent.this, new TimeListener(v),
327 mTime.hour, mTime.minute,
328 DateFormat.is24HourFormat(EditEvent.this)).show();
332 private class DateListener implements OnDateSetListener {
335 public DateListener(View view) {
339 public void onDateSet(DatePicker view, int year, int month, int monthDay) {
340 // Cache the member variables locally to avoid inner class overhead.
341 Time startTime = mStartTime;
342 Time endTime = mEndTime;
344 // Cache the start and end millis so that we limit the number
345 // of calls to normalize() and toMillis(), which are fairly
349 if (mView == mStartDateButton) {
350 // The start date was changed.
351 int yearDuration = endTime.year - startTime.year;
352 int monthDuration = endTime.month - startTime.month;
353 int monthDayDuration = endTime.monthDay - startTime.monthDay;
355 startTime.year = year;
356 startTime.month = month;
357 startTime.monthDay = monthDay;
358 startMillis = startTime.normalize(true);
360 // Also update the end date to keep the duration constant.
361 endTime.year = year + yearDuration;
362 endTime.month = month + monthDuration;
363 endTime.monthDay = monthDay + monthDayDuration;
364 endMillis = endTime.normalize(true);
366 // If the start date has changed then update the repeats.
369 // The end date was changed.
370 startMillis = startTime.toMillis(true);
372 endTime.month = month;
373 endTime.monthDay = monthDay;
374 endMillis = endTime.normalize(true);
376 // Do not allow an event to have an end time before the start time.
377 if (endTime.before(startTime)) {
378 endTime.set(startTime);
379 endMillis = startMillis;
383 setDate(mStartDateButton, startMillis);
384 setDate(mEndDateButton, endMillis);
385 setTime(mEndTimeButton, endMillis); // In case end time had to be reset
389 private class DateClickListener implements View.OnClickListener {
392 public DateClickListener(Time time) {
396 public void onClick(View v) {
397 new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year,
398 mTime.month, mTime.monthDay).show();
402 static private class CalendarsAdapter extends ResourceCursorAdapter {
403 public CalendarsAdapter(Context context, Cursor c) {
404 super(context, R.layout.calendars_item, c);
405 setDropDownViewResource(R.layout.calendars_dropdown_item);
409 public void bindView(View view, Context context, Cursor cursor) {
410 TextView name = (TextView) view.findViewById(R.id.calendar_name);
411 name.setText(cursor.getString(CALENDARS_INDEX_DISPLAY_NAME));
415 // This is called if the user clicks on one of the buttons: "Save",
416 // "Discard", or "Delete". This is also called if the user clicks
417 // on the "remove reminder" button.
418 public void onClick(View v) {
419 if (v == mSaveButton) {
426 if (v == mDeleteButton) {
427 long begin = mStartTime.toMillis(false /* use isDst */);
428 long end = mEndTime.toMillis(false /* use isDst */);
430 switch (mModification) {
431 case MODIFY_SELECTED:
432 which = DeleteEventHelper.DELETE_SELECTED;
434 case MODIFY_ALL_FOLLOWING:
435 which = DeleteEventHelper.DELETE_ALL_FOLLOWING;
438 which = DeleteEventHelper.DELETE_ALL;
441 mDeleteEventHelper.delete(begin, end, mEventCursor, which);
445 if (v == mDiscardButton) {
450 // This must be a click on one of the "remove reminder" buttons
451 LinearLayout reminderItem = (LinearLayout) v.getParent();
452 LinearLayout parent = (LinearLayout) reminderItem.getParent();
453 parent.removeView(reminderItem);
454 mReminderItems.remove(reminderItem);
455 updateRemindersVisibility();
458 // This is called if the user cancels a popup dialog. There are two
459 // dialogs: the "Loading calendars" dialog, and the "No calendars"
460 // dialog. The "Loading calendars" dialog is shown if there is a delay
461 // in loading the calendars (needed when creating an event) and the user
462 // tries to save the event before the calendars have finished loading.
463 // The "No calendars" dialog is shown if there are no syncable calendars.
464 public void onCancel(DialogInterface dialog) {
465 if (dialog == mLoadingCalendarsDialog) {
466 mSaveAfterQueryComplete = false;
467 } else if (dialog == mNoCalendarsDialog) {
472 // This is called if the user clicks on a dialog button.
473 public void onClick(DialogInterface dialog, int which) {
474 if (dialog == mNoCalendarsDialog) {
479 private class QueryHandler extends AsyncQueryHandler {
480 public QueryHandler(ContentResolver cr) {
485 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
486 // If the Activity is finishing, then close the cursor.
487 // Otherwise, use the new cursor in the adapter.
489 stopManagingCursor(cursor);
492 mCalendarsCursor = cursor;
493 startManagingCursor(cursor);
496 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
497 Window.PROGRESS_VISIBILITY_OFF);
499 // If there are no syncable calendars, then we cannot allow
500 // creating a new event.
501 if (cursor.getCount() == 0) {
502 // Cancel the "loading calendars" dialog if it exists
503 if (mSaveAfterQueryComplete) {
504 mLoadingCalendarsDialog.cancel();
507 // Create an error message for the user that, when clicked,
508 // will exit this activity without saving the event.
509 AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
510 builder.setTitle(R.string.no_syncable_calendars)
511 .setIcon(android.R.drawable.ic_dialog_alert)
512 .setMessage(R.string.no_calendars_found)
513 .setPositiveButton(android.R.string.ok, EditEvent.this)
514 .setOnCancelListener(EditEvent.this);
515 mNoCalendarsDialog = builder.show();
519 int primaryCalendarPosition = findPrimaryCalendarPosition();
521 // populate the calendars spinner
522 CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor);
523 mCalendarsSpinner.setAdapter(adapter);
524 mCalendarsSpinner.setSelection(primaryCalendarPosition);
525 mCalendarsQueryComplete = true;
526 if (mSaveAfterQueryComplete) {
527 mLoadingCalendarsDialog.cancel();
532 // Find user domain and set it to the validator.
533 // TODO: we may want to update this validator if the user actually picks
534 // a different calendar. maybe not. depends on what we want for the
535 // user experience. this may change when we add support for multiple
537 if (mHasAttendeeData && cursor.moveToPosition(primaryCalendarPosition)) {
538 String ownEmail = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
539 if (ownEmail != null) {
540 String domain = extractDomain(ownEmail);
541 if (domain != null) {
542 mEmailValidator = new Rfc822Validator(domain);
543 mAttendeesList.setValidator(mEmailValidator);
550 // Find the calendar position in the cursor that matches the signed-in
552 private int findPrimaryCalendarPosition() {
553 int primaryCalendarPosition = -1;
555 Account[] accounts = mAccountManager.getAccountsByTypeAndFeatures(
556 GoogleLoginServiceConstants.ACCOUNT_TYPE, new String[] {
557 GoogleLoginServiceConstants.FEATURE_LEGACY_HOSTED_OR_GOOGLE
558 }, null, null).getResult();
559 if (accounts.length > 0) {
560 for (int i = 0; i < accounts.length && primaryCalendarPosition == -1; ++i) {
561 String name = accounts[i].name;
567 mCalendarsCursor.moveToPosition(-1);
568 while (mCalendarsCursor.moveToNext()) {
569 if (name.equals(mCalendarsCursor
570 .getString(CALENDARS_INDEX_OWNER_ACCOUNT))) {
571 primaryCalendarPosition = position;
578 } catch (OperationCanceledException e) {
579 Log.w(TAG, "Ignoring unexpected exception", e);
580 } catch (IOException e) {
581 Log.w(TAG, "Ignoring unexpected exception", e);
582 } catch (AuthenticatorException e) {
583 Log.w(TAG, "Ignoring unexpected exception", e);
585 if (primaryCalendarPosition != -1) {
586 return primaryCalendarPosition;
594 private static String extractDomain(String email) {
595 int separator = email.lastIndexOf('@');
596 if (separator != -1 && ++separator < email.length()) {
597 return email.substring(separator);
603 protected void onCreate(Bundle icicle) {
604 super.onCreate(icicle);
605 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
606 setContentView(R.layout.edit_event);
607 mAccountManager = AccountManager.get(this);
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);
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 = PreferenceManager.getDefaultSharedPreferences(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 for (Rfc822Token address : addresses) {
881 if (!mEmailValidator.isValid(address.getAddress())) {
882 Log.w(TAG, "Dropping invalid attendee email address: " + address);
883 addresses.remove(address);
889 // From com.google.android.gm.ComposeActivity
890 private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res) {
891 MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res);
892 list.setAdapter(mAddressAdapter);
893 list.setTokenizer(new Rfc822Tokenizer());
894 list.setValidator(mEmailValidator);
896 // NOTE: assumes no other filters are set
897 list.setFilters(sRecipientFilters);
903 * From com.google.android.gm.ComposeActivity
904 * Implements special address cleanup rules:
905 * The first space key entry following an "@" symbol that is followed by any combination
906 * of letters and symbols, including one+ dots and zero commas, should insert an extra
907 * comma (followed by the space).
909 private static InputFilter[] sRecipientFilters = new InputFilter[] { new Rfc822InputFilter() };
911 private void initFromIntent(Intent intent) {
912 String title = intent.getStringExtra(Events.TITLE);
914 mTitleTextView.setText(title);
917 String location = intent.getStringExtra(Events.EVENT_LOCATION);
918 if (location != null) {
919 mLocationTextView.setText(location);
922 String description = intent.getStringExtra(Events.DESCRIPTION);
923 if (description != null) {
924 mDescriptionTextView.setText(description);
927 int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
928 if (availability != -1) {
929 mAvailabilitySpinner.setSelection(availability);
932 int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
933 if (visibility != -1) {
934 mVisibilitySpinner.setSelection(visibility);
937 String rrule = intent.getStringExtra(Events.RRULE);
940 mEventRecurrence.parse(rrule);
945 protected void onResume() {
949 if (mEventCursor == null || mEventCursor.getCount() == 0) {
950 // The cursor is empty. This can happen if the event was deleted.
956 if (mEventCursor != null) {
957 Cursor cursor = mEventCursor;
958 cursor.moveToFirst();
960 mRrule = cursor.getString(EVENT_INDEX_RRULE);
961 String title = cursor.getString(EVENT_INDEX_TITLE);
962 String description = cursor.getString(EVENT_INDEX_DESCRIPTION);
963 String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
964 int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY);
965 int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY);
966 if (visibility > 0) {
967 // For now we the array contains the values 0, 2, and 3. We subtract one to match.
971 if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) {
972 // If this event has not been synced, then don't allow deleting
973 // or changing a single instance.
974 mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
975 mEventRecurrence.parse(mRrule);
977 // If we haven't synced this repeating event yet, then don't
978 // allow the user to change just one instance.
980 CharSequence[] items;
981 if (mSyncId == null) {
982 items = new CharSequence[2];
984 items = new CharSequence[3];
985 items[itemIndex++] = getText(R.string.modify_event);
987 items[itemIndex++] = getText(R.string.modify_all);
988 items[itemIndex++] = getText(R.string.modify_all_following);
990 // Display the modification dialog.
991 new AlertDialog.Builder(this)
992 .setOnCancelListener(new OnCancelListener() {
993 public void onCancel(DialogInterface dialog) {
997 .setTitle(R.string.edit_event_label)
998 .setItems(items, new OnClickListener() {
999 public void onClick(DialogInterface dialog, int which) {
1002 (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED;
1003 } else if (which == 1) {
1005 (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL;
1006 } else if (which == 2) {
1007 mModification = MODIFY_ALL_FOLLOWING;
1010 // If we are modifying all the events in a
1011 // series then disable and ignore the date.
1012 if (mModification == MODIFY_ALL) {
1013 mStartDateButton.setEnabled(false);
1014 mEndDateButton.setEnabled(false);
1015 } else if (mModification == MODIFY_SELECTED) {
1016 mRepeatsSpinner.setEnabled(false);
1023 mTitleTextView.setText(title);
1024 mLocationTextView.setText(location);
1025 mDescriptionTextView.setText(description);
1026 mAvailabilitySpinner.setSelection(availability);
1027 mVisibilitySpinner.setSelection(visibility);
1029 // This is an existing event so hide the calendar spinner
1030 // since we can't change the calendar.
1031 View calendarGroup = findViewById(R.id.calendar_group);
1032 calendarGroup.setVisibility(View.GONE);
1035 if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) {
1036 mStartTime.setToNow();
1038 // Round the time to the nearest half hour.
1039 mStartTime.second = 0;
1040 int minute = mStartTime.minute;
1041 if (minute > 0 && minute <= 30) {
1042 mStartTime.minute = 30;
1044 mStartTime.minute = 0;
1045 mStartTime.hour += 1;
1048 long startMillis = mStartTime.normalize(true /* ignore isDst */);
1049 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
1052 // Hide delete button
1053 mDeleteButton.setVisibility(View.GONE);
1056 updateRemindersVisibility();
1062 public boolean onCreateOptionsMenu(Menu menu) {
1064 item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
1065 R.string.add_new_reminder);
1066 item.setIcon(R.drawable.ic_menu_reminder);
1067 item.setAlphabeticShortcut('r');
1069 item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
1070 R.string.edit_event_show_extra_options);
1071 item.setIcon(R.drawable.ic_menu_show_list);
1072 item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
1073 R.string.edit_event_hide_extra_options);
1074 item.setIcon(R.drawable.ic_menu_show_list);
1076 return super.onCreateOptionsMenu(menu);
1080 public boolean onPrepareOptionsMenu(Menu menu) {
1081 if (mReminderItems.size() < MAX_REMINDERS) {
1082 menu.setGroupVisible(MENU_GROUP_REMINDER, true);
1083 menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
1085 menu.setGroupVisible(MENU_GROUP_REMINDER, false);
1086 menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
1089 if (mExtraOptions.getVisibility() == View.VISIBLE) {
1090 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
1091 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
1093 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
1094 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
1097 return super.onPrepareOptionsMenu(menu);
1100 private void addReminder() {
1101 // TODO: when adding a new reminder, make it different from the
1102 // last one in the list (if any).
1103 if (mDefaultReminderMinutes == 0) {
1104 addReminder(this, this, mReminderItems, mReminderValues,
1105 mReminderLabels, 10 /* minutes */);
1107 addReminder(this, this, mReminderItems, mReminderValues,
1108 mReminderLabels, mDefaultReminderMinutes);
1110 updateRemindersVisibility();
1114 public boolean onOptionsItemSelected(MenuItem item) {
1115 switch (item.getItemId()) {
1116 case MENU_ADD_REMINDER:
1119 case MENU_SHOW_EXTRA_OPTIONS:
1120 mExtraOptions.setVisibility(View.VISIBLE);
1122 case MENU_HIDE_EXTRA_OPTIONS:
1123 mExtraOptions.setVisibility(View.GONE);
1126 return super.onOptionsItemSelected(item);
1130 public void onBackPressed() {
1131 // If we are creating a new event, do not create it if the
1132 // title, location and description are all empty, in order to
1133 // prevent accidental "no subject" event creations.
1134 if (mUri != null || !isEmpty()) {
1136 // We cannot exit this activity because the calendars
1137 // are still loading.
1144 private void populateWhen() {
1145 long startMillis = mStartTime.toMillis(false /* use isDst */);
1146 long endMillis = mEndTime.toMillis(false /* use isDst */);
1147 setDate(mStartDateButton, startMillis);
1148 setDate(mEndDateButton, endMillis);
1150 setTime(mStartTimeButton, startMillis);
1151 setTime(mEndTimeButton, endMillis);
1153 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
1154 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
1156 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
1157 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
1160 private void populateRepeats() {
1161 Time time = mStartTime;
1162 Resources r = getResources();
1163 int resource = android.R.layout.simple_spinner_item;
1165 String[] days = new String[] {
1166 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
1167 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
1168 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
1169 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
1170 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
1171 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
1172 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
1174 String[] ordinals = r.getStringArray(R.array.ordinal_labels);
1176 // Only display "Custom" in the spinner if the device does not support the
1177 // recurrence functionality of the event. Only display every weekday if
1178 // the event starts on a weekday.
1179 boolean isCustomRecurrence = isCustomRecurrence();
1180 boolean isWeekdayEvent = isWeekdayEvent();
1182 ArrayList<String> repeatArray = new ArrayList<String>(0);
1183 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
1185 repeatArray.add(r.getString(R.string.does_not_repeat));
1186 recurrenceIndexes.add(DOES_NOT_REPEAT);
1188 repeatArray.add(r.getString(R.string.daily));
1189 recurrenceIndexes.add(REPEATS_DAILY);
1191 if (isWeekdayEvent) {
1192 repeatArray.add(r.getString(R.string.every_weekday));
1193 recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
1196 String format = r.getString(R.string.weekly);
1197 repeatArray.add(String.format(format, time.format("%A")));
1198 recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
1200 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
1201 int dayNumber = (time.monthDay - 1) / 7;
1202 format = r.getString(R.string.monthly_on_day_count);
1203 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
1204 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
1206 format = r.getString(R.string.monthly_on_day);
1207 repeatArray.add(String.format(format, time.monthDay));
1208 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
1210 long when = time.toMillis(false);
1211 format = r.getString(R.string.yearly);
1213 if (DateFormat.is24HourFormat(this)) {
1214 flags |= DateUtils.FORMAT_24HOUR;
1216 repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
1217 recurrenceIndexes.add(REPEATS_YEARLY);
1219 if (isCustomRecurrence) {
1220 repeatArray.add(r.getString(R.string.custom));
1221 recurrenceIndexes.add(REPEATS_CUSTOM);
1223 mRecurrenceIndexes = recurrenceIndexes;
1225 int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
1226 if (mRrule != null) {
1227 if (isCustomRecurrence) {
1228 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
1230 switch (mEventRecurrence.freq) {
1231 case EventRecurrence.DAILY:
1232 position = recurrenceIndexes.indexOf(REPEATS_DAILY);
1234 case EventRecurrence.WEEKLY:
1235 if (mEventRecurrence.repeatsOnEveryWeekDay()) {
1236 position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
1238 position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
1241 case EventRecurrence.MONTHLY:
1242 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1243 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
1245 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
1248 case EventRecurrence.YEARLY:
1249 position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
1254 ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
1255 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1256 mRepeatsSpinner.setAdapter(adapter);
1257 mRepeatsSpinner.setSelection(position);
1260 // Adds a reminder to the displayed list of reminders.
1261 // Returns true if successfully added reminder, false if no reminders can
1263 static boolean addReminder(Activity activity, View.OnClickListener listener,
1264 ArrayList<LinearLayout> items, ArrayList<Integer> values,
1265 ArrayList<String> labels, int minutes) {
1267 if (items.size() >= MAX_REMINDERS) {
1271 LayoutInflater inflater = activity.getLayoutInflater();
1272 LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
1273 LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
1274 parent.addView(reminderItem);
1276 Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
1277 Resources res = activity.getResources();
1278 spinner.setPrompt(res.getString(R.string.reminders_label));
1279 int resource = android.R.layout.simple_spinner_item;
1280 ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
1281 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1282 spinner.setAdapter(adapter);
1284 ImageButton reminderRemoveButton;
1285 reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
1286 reminderRemoveButton.setOnClickListener(listener);
1288 int index = findMinutesInReminderList(values, minutes);
1289 spinner.setSelection(index);
1290 items.add(reminderItem);
1295 static void addMinutesToList(Context context, ArrayList<Integer> values,
1296 ArrayList<String> labels, int minutes) {
1297 int index = values.indexOf(minutes);
1302 // The requested "minutes" does not exist in the list, so insert it
1305 String label = constructReminderLabel(context, minutes, false);
1306 int len = values.size();
1307 for (int i = 0; i < len; i++) {
1308 if (minutes < values.get(i)) {
1309 values.add(i, minutes);
1310 labels.add(i, label);
1315 values.add(minutes);
1316 labels.add(len, label);
1320 * Finds the index of the given "minutes" in the "values" list.
1322 * @param values the list of minutes corresponding to the spinner choices
1323 * @param minutes the minutes to search for in the values list
1324 * @return the index of "minutes" in the "values" list
1326 private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
1327 int index = values.indexOf(minutes);
1329 // This should never happen.
1330 Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
1336 // Constructs a label given an arbitrary number of minutes. For example,
1337 // if the given minutes is 63, then this returns the string "63 minutes".
1338 // As another example, if the given minutes is 120, then this returns
1340 static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
1341 Resources resources = context.getResources();
1344 if (minutes % 60 != 0) {
1347 resId = R.plurals.Nmins;
1349 resId = R.plurals.Nminutes;
1351 } else if (minutes % (24 * 60) != 0) {
1352 value = minutes / 60;
1353 resId = R.plurals.Nhours;
1355 value = minutes / ( 24 * 60);
1356 resId = R.plurals.Ndays;
1359 String format = resources.getQuantityString(resId, value);
1360 return String.format(format, value);
1363 private void updateRemindersVisibility() {
1364 if (mReminderItems.size() == 0) {
1365 mRemindersSeparator.setVisibility(View.GONE);
1366 mRemindersContainer.setVisibility(View.GONE);
1368 mRemindersSeparator.setVisibility(View.VISIBLE);
1369 mRemindersContainer.setVisibility(View.VISIBLE);
1373 private void setDate(TextView view, long millis) {
1374 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
1375 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
1376 DateUtils.FORMAT_ABBREV_WEEKDAY;
1377 view.setText(DateUtils.formatDateTime(this, millis, flags));
1380 private void setTime(TextView view, long millis) {
1381 int flags = DateUtils.FORMAT_SHOW_TIME;
1382 if (DateFormat.is24HourFormat(this)) {
1383 flags |= DateUtils.FORMAT_24HOUR;
1385 view.setText(DateUtils.formatDateTime(this, millis, flags));
1388 // Saves the event. Returns true if it is okay to exit this activity.
1389 private boolean save() {
1390 boolean forceSaveReminders = false;
1392 // If we are creating a new event, then make sure we wait until the
1393 // query to fetch the list of calendars has finished.
1394 if (mEventCursor == null) {
1395 if (!mCalendarsQueryComplete) {
1396 // Wait for the calendars query to finish.
1397 if (mLoadingCalendarsDialog == null) {
1398 // Create the progress dialog
1399 mLoadingCalendarsDialog = ProgressDialog.show(this,
1400 getText(R.string.loading_calendars_title),
1401 getText(R.string.loading_calendars_message),
1403 mSaveAfterQueryComplete = true;
1408 // Avoid creating a new event if the calendars cursor is empty. This
1409 // shouldn't ever happen since the setup wizard should ensure the user
1411 if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0) {
1412 Log.w("Cal", "The calendars table does not contain any calendars."
1413 + " New event was not created.");
1416 Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
1418 Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
1421 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1422 int eventIdIndex = -1;
1424 ContentValues values = getContentValuesFromUi();
1427 // Update the "hasAlarm" field for the event
1428 ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
1430 int len = reminderMinutes.size();
1431 values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
1433 // For recurring events, we must make sure that we use duration rather
1436 // Add hasAttendeeData for a new event
1437 values.put(Events.HAS_ATTENDEE_DATA, 1);
1438 // Create new event with new contents
1439 addRecurrenceRule(values);
1440 eventIdIndex = ops.size();
1441 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1443 forceSaveReminders = true;
1445 } else if (mRrule == null) {
1446 // Modify contents of a non-repeating event
1447 addRecurrenceRule(values);
1448 checkTimeDependentFields(values);
1449 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1451 } else if (mInitialValues.getAsString(Events.RRULE) == null) {
1452 // This event was changed from a non-repeating event to a
1454 addRecurrenceRule(values);
1455 values.remove(Events.DTEND);
1456 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1458 } else if (mModification == MODIFY_SELECTED) {
1459 // Modify contents of the current instance of repeating event
1461 // Create a recurrence exception
1462 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1463 values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
1464 values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
1465 boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1466 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
1468 eventIdIndex = ops.size();
1469 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1471 forceSaveReminders = true;
1473 } else if (mModification == MODIFY_ALL_FOLLOWING) {
1474 // Modify this instance and all future instances of repeating event
1475 addRecurrenceRule(values);
1477 if (mRrule == null) {
1478 // We've changed a recurring event to a non-recurring event.
1479 // If the event we are editing is the first in the series,
1480 // then delete the whole series. Otherwise, update the series
1481 // to end at the new start time.
1482 if (isFirstEventInSeries()) {
1483 ops.add(ContentProviderOperation.newDelete(uri).build());
1485 // Update the current repeating event to end at the new
1487 updatePastEvents(ops, uri);
1489 eventIdIndex = ops.size();
1490 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1493 if (isFirstEventInSeries()) {
1494 checkTimeDependentFields(values);
1495 values.remove(Events.DTEND);
1496 Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
1499 // Update the current repeating event to end at the new
1501 updatePastEvents(ops, uri);
1503 // Create a new event with the user-modified fields
1504 values.remove(Events.DTEND);
1505 eventIdIndex = ops.size();
1506 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
1510 forceSaveReminders = true;
1512 } else if (mModification == MODIFY_ALL) {
1514 // Modify all instances of repeating event
1515 addRecurrenceRule(values);
1517 if (mRrule == null) {
1518 // We've changed a recurring event to a non-recurring event.
1519 // Delete the whole series and replace it with a new
1520 // non-recurring event.
1521 ops.add(ContentProviderOperation.newDelete(uri).build());
1523 eventIdIndex = ops.size();
1524 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1526 forceSaveReminders = true;
1528 checkTimeDependentFields(values);
1529 values.remove(Events.DTEND);
1530 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1534 if (eventIdIndex != -1) {
1535 saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
1536 forceSaveReminders);
1537 } else if (uri != null) {
1538 long eventId = ContentUris.parseId(uri);
1539 saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
1540 forceSaveReminders);
1545 // New event/instance - Set Organizer's response as yes
1546 if (mHasAttendeeData && eventIdIndex != -1) {
1548 int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
1549 String ownerEmail = mOwnerAccount;
1550 // Just in case mOwnerAccount is null, try to get owner from mCalendarsCursor
1551 if (ownerEmail == null && mCalendarsCursor != null &&
1552 mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
1553 ownerEmail = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
1555 if (ownerEmail != null) {
1556 values.put(Attendees.ATTENDEE_EMAIL, ownerEmail);
1557 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
1558 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1559 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
1561 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1562 .withValues(values);
1563 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1568 // TODO: is this the right test? this currently checks if this is
1569 // a new event or an existing event. or is this a paranoia check?
1570 if (mHasAttendeeData && (eventIdIndex != -1 || uri != null)) {
1571 Editable attendeesText = mAttendeesList.getText();
1572 // Hit the content provider only if the user has changed it
1573 if (!mOriginalAttendees.equals(attendeesText.toString())) {
1574 // figure out which attendees need to be added and which ones
1575 // need to be deleted. use a linked hash set, so we maintain
1576 // order (but also remove duplicates).
1577 LinkedHashSet<Rfc822Token> newAttendees = getAddressesFromList(mAttendeesList);
1579 // the eventId is only used if eventIdIndex is -1.
1580 // TODO: clean up this code.
1581 long eventId = uri != null ? ContentUris.parseId(uri) : -1;
1583 // only compute deltas if this is an existing event.
1584 // new events (being inserted into the Events table) won't
1585 // have any existing attendees.
1586 if (eventIdIndex == -1) {
1587 HashSet<Rfc822Token> removedAttendees = new HashSet<Rfc822Token>();
1588 HashSet<Rfc822Token> originalAttendees = new HashSet<Rfc822Token>();
1589 Rfc822Tokenizer.tokenize(mOriginalAttendees, originalAttendees);
1590 for (Rfc822Token originalAttendee : originalAttendees) {
1591 if (newAttendees.contains(originalAttendee)) {
1592 // existing attendee. remove from new attendees set.
1593 newAttendees.remove(originalAttendee);
1595 // no longer in attendees. mark as removed.
1596 removedAttendees.add(originalAttendee);
1600 // delete removed attendees
1601 b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
1603 String[] args = new String[removedAttendees.size() + 1];
1604 args[0] = Long.toString(eventId);
1606 StringBuilder deleteWhere = new StringBuilder(ATTENDEES_DELETE_PREFIX);
1607 for (Rfc822Token removedAttendee : removedAttendees) {
1609 deleteWhere.append(",");
1611 deleteWhere.append("?");
1612 args[i++] = removedAttendee.getAddress();
1614 deleteWhere.append(")");
1615 b.withSelection(deleteWhere.toString(), args);
1619 if (newAttendees.size() > 0) {
1620 // Insert the new attendees
1621 for (Rfc822Token attendee : newAttendees) {
1623 values.put(Attendees.ATTENDEE_NAME, attendee.getName());
1624 values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
1625 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
1626 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1627 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
1629 if (eventIdIndex != -1) {
1630 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1631 .withValues(values);
1632 b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex);
1634 values.put(Attendees.EVENT_ID, eventId);
1635 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1636 .withValues(values);
1645 // TODO Move this to background thread
1646 ContentProviderResult[] results =
1647 getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
1649 for (int i = 0; i < results.length; i++) {
1650 Log.v(TAG, "results = " + results[i].toString());
1653 } catch (RemoteException e) {
1654 Log.w(TAG, "Ignoring unexpected remote exception", e);
1655 } catch (OperationApplicationException e) {
1656 Log.w(TAG, "Ignoring unexpected exception", e);
1662 private boolean isFirstEventInSeries() {
1663 int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
1664 long start = mEventCursor.getLong(dtStart);
1665 return start == mStartTime.toMillis(true);
1668 private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) {
1669 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1670 String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
1671 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1672 String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
1673 mEventRecurrence.parse(oldRrule);
1675 Time untilTime = new Time();
1676 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1677 ContentValues oldValues = new ContentValues();
1679 // The "until" time must be in UTC time in order for Google calendar
1680 // to display it properly. For all-day events, the "until" time string
1681 // must include just the date field, and not the time field. The
1682 // repeating events repeat up to and including the "until" time.
1683 untilTime.timezone = Time.TIMEZONE_UTC;
1685 // Subtract one second from the old begin time to get the new
1687 untilTime.set(begin - 1000); // subtract one second (1000 millis)
1690 untilTime.minute = 0;
1691 untilTime.second = 0;
1692 untilTime.allDay = true;
1693 untilTime.normalize(false);
1695 // For all-day events, the duration must be in days, not seconds.
1696 // Otherwise, Google Calendar will (mistakenly) change this event
1697 // into a non-all-day event.
1698 int len = oldDuration.length();
1699 if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
1700 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
1701 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1702 oldDuration = "P" + days + "D";
1705 mEventRecurrence.until = untilTime.format2445();
1707 oldValues.put(Events.DTSTART, oldStartMillis);
1708 oldValues.put(Events.DURATION, oldDuration);
1709 oldValues.put(Events.RRULE, mEventRecurrence.toString());
1710 Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
1714 private void checkTimeDependentFields(ContentValues values) {
1715 long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1716 long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
1717 boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1718 String oldRrule = mInitialValues.getAsString(Events.RRULE);
1719 String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
1721 long newBegin = values.getAsLong(Events.DTSTART);
1722 long newEnd = values.getAsLong(Events.DTEND);
1723 boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
1724 String newRrule = values.getAsString(Events.RRULE);
1725 String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1727 // If none of the time-dependent fields changed, then remove them.
1728 if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
1729 && TextUtils.equals(oldRrule, newRrule)
1730 && TextUtils.equals(oldTimezone, newTimezone)) {
1731 values.remove(Events.DTSTART);
1732 values.remove(Events.DTEND);
1733 values.remove(Events.DURATION);
1734 values.remove(Events.ALL_DAY);
1735 values.remove(Events.RRULE);
1736 values.remove(Events.EVENT_TIMEZONE);
1740 if (oldRrule == null || newRrule == null) {
1744 // If we are modifying all events then we need to set DTSTART to the
1745 // start time of the first event in the series, not the current
1746 // date and time. If the start time of the event was changed
1747 // (from, say, 3pm to 4pm), then we want to add the time difference
1748 // to the start time of the first event in the series (the DTSTART
1749 // value). If we are modifying one instance or all following instances,
1750 // then we leave the DTSTART field alone.
1751 if (mModification == MODIFY_ALL) {
1752 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1753 if (oldBegin != newBegin) {
1754 // The user changed the start time of this event
1755 long offset = newBegin - oldBegin;
1756 oldStartMillis += offset;
1758 values.put(Events.DTSTART, oldStartMillis);
1762 static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
1763 ArrayList<Integer> reminderValues) {
1764 int len = reminderItems.size();
1765 ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
1766 for (int index = 0; index < len; index++) {
1767 LinearLayout layout = reminderItems.get(index);
1768 Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
1769 int minutes = reminderValues.get(spinner.getSelectedItemPosition());
1770 reminderMinutes.add(minutes);
1772 return reminderMinutes;
1776 * Saves the reminders, if they changed. Returns true if the database
1779 * @param ops the array of ContentProviderOperations
1780 * @param eventId the id of the event whose reminders are being updated
1781 * @param reminderMinutes the array of reminders set by the user
1782 * @param originalMinutes the original array of reminders
1783 * @param forceSave if true, then save the reminders even if they didn't
1785 * @return true if the database was updated
1787 static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
1788 ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
1789 boolean forceSave) {
1790 // If the reminders have not changed, then don't update the database
1791 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1795 // Delete all the existing reminders for this event
1796 String where = Reminders.EVENT_ID + "=?";
1797 String[] args = new String[] { Long.toString(eventId) };
1798 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1799 b.withSelection(where, args);
1802 ContentValues values = new ContentValues();
1803 int len = reminderMinutes.size();
1805 // Insert the new reminders, if any
1806 for (int i = 0; i < len; i++) {
1807 int minutes = reminderMinutes.get(i);
1810 values.put(Reminders.MINUTES, minutes);
1811 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1812 values.put(Reminders.EVENT_ID, eventId);
1813 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1819 static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
1820 int eventIdIndex, ArrayList<Integer> reminderMinutes,
1821 ArrayList<Integer> originalMinutes, boolean forceSave) {
1822 // If the reminders have not changed, then don't update the database
1823 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1827 // Delete all the existing reminders for this event
1828 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1829 b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
1830 b.withSelectionBackReference(0, eventIdIndex);
1833 ContentValues values = new ContentValues();
1834 int len = reminderMinutes.size();
1836 // Insert the new reminders, if any
1837 for (int i = 0; i < len; i++) {
1838 int minutes = reminderMinutes.get(i);
1841 values.put(Reminders.MINUTES, minutes);
1842 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1843 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1844 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1850 private void addRecurrenceRule(ContentValues values) {
1851 updateRecurrenceRule();
1853 if (mRrule == null) {
1857 values.put(Events.RRULE, mRrule);
1858 long end = mEndTime.toMillis(true /* ignore dst */);
1859 long start = mStartTime.toMillis(true /* ignore dst */);
1862 boolean isAllDay = mAllDayCheckBox.isChecked();
1864 long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
1865 duration = "P" + days + "D";
1867 long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
1868 duration = "P" + seconds + "S";
1870 values.put(Events.DURATION, duration);
1873 private void updateRecurrenceRule() {
1874 int position = mRepeatsSpinner.getSelectedItemPosition();
1875 int selection = mRecurrenceIndexes.get(position);
1877 if (selection == DOES_NOT_REPEAT) {
1880 } else if (selection == REPEATS_CUSTOM) {
1881 // Keep custom recurrence as before.
1883 } else if (selection == REPEATS_DAILY) {
1884 mEventRecurrence.freq = EventRecurrence.DAILY;
1885 } else if (selection == REPEATS_EVERY_WEEKDAY) {
1886 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1888 int[] byday = new int[dayCount];
1889 int[] bydayNum = new int[dayCount];
1891 byday[0] = EventRecurrence.MO;
1892 byday[1] = EventRecurrence.TU;
1893 byday[2] = EventRecurrence.WE;
1894 byday[3] = EventRecurrence.TH;
1895 byday[4] = EventRecurrence.FR;
1896 for (int day = 0; day < dayCount; day++) {
1900 mEventRecurrence.byday = byday;
1901 mEventRecurrence.bydayNum = bydayNum;
1902 mEventRecurrence.bydayCount = dayCount;
1903 } else if (selection == REPEATS_WEEKLY_ON_DAY) {
1904 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1905 int[] days = new int[1];
1907 int[] dayNum = new int[dayCount];
1909 days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1910 // not sure why this needs to be zero, but set it for now.
1913 mEventRecurrence.byday = days;
1914 mEventRecurrence.bydayNum = dayNum;
1915 mEventRecurrence.bydayCount = dayCount;
1916 } else if (selection == REPEATS_MONTHLY_ON_DAY) {
1917 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1918 mEventRecurrence.bydayCount = 0;
1919 mEventRecurrence.bymonthdayCount = 1;
1920 int[] bymonthday = new int[1];
1921 bymonthday[0] = mStartTime.monthDay;
1922 mEventRecurrence.bymonthday = bymonthday;
1923 } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
1924 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1925 mEventRecurrence.bydayCount = 1;
1926 mEventRecurrence.bymonthdayCount = 0;
1928 int[] byday = new int[1];
1929 int[] bydayNum = new int[1];
1930 // Compute the week number (for example, the "2nd" Monday)
1931 int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
1932 if (dayCount == 5) {
1935 bydayNum[0] = dayCount;
1936 byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1937 mEventRecurrence.byday = byday;
1938 mEventRecurrence.bydayNum = bydayNum;
1939 } else if (selection == REPEATS_YEARLY) {
1940 mEventRecurrence.freq = EventRecurrence.YEARLY;
1943 // Set the week start day.
1944 mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
1945 mRrule = mEventRecurrence.toString();
1948 private ContentValues getContentValuesFromUi() {
1949 String title = mTitleTextView.getText().toString();
1950 boolean isAllDay = mAllDayCheckBox.isChecked();
1951 String location = mLocationTextView.getText().toString();
1952 String description = mDescriptionTextView.getText().toString();
1954 ContentValues values = new ContentValues();
1956 String timezone = null;
1961 // Reset start and end time, increment the monthDay by 1, and set
1962 // the timezone to UTC, as required for all-day events.
1963 timezone = Time.TIMEZONE_UTC;
1964 mStartTime.hour = 0;
1965 mStartTime.minute = 0;
1966 mStartTime.second = 0;
1967 mStartTime.timezone = timezone;
1968 startMillis = mStartTime.normalize(true);
1971 mEndTime.minute = 0;
1972 mEndTime.second = 0;
1973 mEndTime.monthDay++;
1974 mEndTime.timezone = timezone;
1975 endMillis = mEndTime.normalize(true);
1977 if (mEventCursor == null) {
1978 // This is a new event
1979 calendarId = mCalendarsSpinner.getSelectedItemId();
1981 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1984 startMillis = mStartTime.toMillis(true);
1985 endMillis = mEndTime.toMillis(true);
1986 if (mEventCursor != null) {
1987 // This is an existing event
1988 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
1990 // The timezone might be null if we are changing an existing
1991 // all-day event to a non-all-day event. We need to assign
1992 // a timezone to the non-all-day event.
1993 if (TextUtils.isEmpty(timezone)) {
1994 timezone = TimeZone.getDefault().getID();
1996 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1998 // This is a new event
1999 calendarId = mCalendarsSpinner.getSelectedItemId();
2001 // The timezone for a new event is the currently displayed
2002 // timezone, NOT the timezone of the containing calendar.
2003 timezone = TimeZone.getDefault().getID();
2007 values.put(Events.CALENDAR_ID, calendarId);
2008 values.put(Events.EVENT_TIMEZONE, timezone);
2009 values.put(Events.TITLE, title);
2010 values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
2011 values.put(Events.DTSTART, startMillis);
2012 values.put(Events.DTEND, endMillis);
2013 values.put(Events.DESCRIPTION, description);
2014 values.put(Events.EVENT_LOCATION, location);
2015 values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
2017 int visibility = mVisibilitySpinner.getSelectedItemPosition();
2018 if (visibility > 0) {
2019 // For now we the array contains the values 0, 2, and 3. We add one to match.
2022 values.put(Events.VISIBILITY, visibility);
2027 private boolean isEmpty() {
2028 String title = mTitleTextView.getText().toString();
2029 if (title.length() > 0) {
2033 String location = mLocationTextView.getText().toString();
2034 if (location.length() > 0) {
2038 String description = mDescriptionTextView.getText().toString();
2039 if (description.length() > 0) {
2046 private boolean isCustomRecurrence() {
2048 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
2052 if (mEventRecurrence.freq == 0) {
2056 switch (mEventRecurrence.freq) {
2057 case EventRecurrence.DAILY:
2059 case EventRecurrence.WEEKLY:
2060 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
2062 } else if (mEventRecurrence.bydayCount == 1) {
2066 case EventRecurrence.MONTHLY:
2067 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
2069 } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
2073 case EventRecurrence.YEARLY:
2080 private boolean isWeekdayEvent() {
2081 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {