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.app.Activity;
23 import android.app.AlertDialog;
24 import android.app.DatePickerDialog;
25 import android.app.ProgressDialog;
26 import android.app.TimePickerDialog;
27 import android.app.DatePickerDialog.OnDateSetListener;
28 import android.app.TimePickerDialog.OnTimeSetListener;
29 import android.content.AsyncQueryHandler;
30 import android.content.ContentProviderOperation;
31 import android.content.ContentProviderResult;
32 import android.content.ContentResolver;
33 import android.content.ContentUris;
34 import android.content.ContentValues;
35 import android.content.Context;
36 import android.content.DialogInterface;
37 import android.content.Intent;
38 import android.content.OperationApplicationException;
39 import android.content.SharedPreferences;
40 import android.content.ContentProviderOperation.Builder;
41 import android.content.DialogInterface.OnCancelListener;
42 import android.content.DialogInterface.OnClickListener;
43 import android.content.res.Resources;
44 import android.database.Cursor;
45 import android.net.Uri;
46 import android.os.Bundle;
47 import android.os.RemoteException;
48 import android.pim.EventRecurrence;
49 import android.preference.PreferenceManager;
50 import android.provider.Calendar.Attendees;
51 import android.provider.Calendar.Calendars;
52 import android.provider.Calendar.Events;
53 import android.provider.Calendar.Reminders;
54 import android.text.Editable;
55 import android.text.InputFilter;
56 import android.text.SpannableStringBuilder;
57 import android.text.Spanned;
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.text.util.Rfc822Validator;
65 import android.util.Log;
66 import android.view.KeyEvent;
67 import android.view.LayoutInflater;
68 import android.view.Menu;
69 import android.view.MenuItem;
70 import android.view.View;
71 import android.view.Window;
72 import android.widget.ArrayAdapter;
73 import android.widget.Button;
74 import android.widget.CheckBox;
75 import android.widget.CompoundButton;
76 import android.widget.DatePicker;
77 import android.widget.ImageButton;
78 import android.widget.LinearLayout;
79 import android.widget.MultiAutoCompleteTextView;
80 import android.widget.ResourceCursorAdapter;
81 import android.widget.Spinner;
82 import android.widget.TextView;
83 import android.widget.TimePicker;
84 import android.widget.Toast;
86 import java.util.ArrayList;
87 import java.util.Arrays;
88 import java.util.Calendar;
89 import java.util.TimeZone;
91 public class EditEvent extends Activity implements View.OnClickListener,
92 DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
93 private static final String TAG = "EditEvent";
94 private static final boolean DEBUG = false;
97 * This is the symbolic name for the key used to pass in the boolean
98 * for creating all-day events that is part of the extra data of the intent.
99 * This is used only for creating new events and is set to true if
100 * the default for the new event should be an all-day event.
102 public static final String EVENT_ALL_DAY = "allDay";
104 private static final int MAX_REMINDERS = 5;
106 private static final int MENU_GROUP_REMINDER = 1;
107 private static final int MENU_GROUP_SHOW_OPTIONS = 2;
108 private static final int MENU_GROUP_HIDE_OPTIONS = 3;
110 private static final int MENU_ADD_REMINDER = 1;
111 private static final int MENU_SHOW_EXTRA_OPTIONS = 2;
112 private static final int MENU_HIDE_EXTRA_OPTIONS = 3;
114 private static final String[] EVENT_PROJECTION = new String[] {
117 Events.DESCRIPTION, // 2
118 Events.EVENT_LOCATION, // 3
120 Events.HAS_ALARM, // 5
121 Events.CALENDAR_ID, // 6
123 Events.DURATION, // 8
124 Events.EVENT_TIMEZONE, // 9
126 Events._SYNC_ID, // 11
127 Events.TRANSPARENCY, // 12
128 Events.VISIBILITY, // 13
130 private static final int EVENT_INDEX_ID = 0;
131 private static final int EVENT_INDEX_TITLE = 1;
132 private static final int EVENT_INDEX_DESCRIPTION = 2;
133 private static final int EVENT_INDEX_EVENT_LOCATION = 3;
134 private static final int EVENT_INDEX_ALL_DAY = 4;
135 private static final int EVENT_INDEX_HAS_ALARM = 5;
136 private static final int EVENT_INDEX_CALENDAR_ID = 6;
137 private static final int EVENT_INDEX_DTSTART = 7;
138 private static final int EVENT_INDEX_DURATION = 8;
139 private static final int EVENT_INDEX_TIMEZONE = 9;
140 private static final int EVENT_INDEX_RRULE = 10;
141 private static final int EVENT_INDEX_SYNC_ID = 11;
142 private static final int EVENT_INDEX_TRANSPARENCY = 12;
143 private static final int EVENT_INDEX_VISIBILITY = 13;
145 private static final String[] CALENDARS_PROJECTION = new String[] {
147 Calendars.DISPLAY_NAME, // 1
148 Calendars.OWNER_ACCOUNT, // 2
150 private static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
151 private static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
152 private static final String CALENDARS_WHERE = Calendars.ACCESS_LEVEL + ">=" +
153 Calendars.CONTRIBUTOR_ACCESS + " AND " + Calendars.SYNC_EVENTS + "=1";
155 private static final String[] REMINDERS_PROJECTION = new String[] {
157 Reminders.MINUTES, // 1
159 private static final int REMINDERS_INDEX_MINUTES = 1;
160 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
161 Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
162 Reminders.METHOD_DEFAULT + ")";
164 private static final String[] ATTENDEES_PROJECTION = new String[] {
165 Attendees.ATTENDEE_NAME, // 0
166 Attendees.ATTENDEE_EMAIL, // 1
168 private static final int ATTENDEES_INDEX_NAME = 0;
169 private static final int ATTENDEES_INDEX_EMAIL = 1;
170 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=? AND "
171 + Attendees.ATTENDEE_RELATIONSHIP + "<>" + Attendees.RELATIONSHIP_ORGANIZER;
173 private static final int DOES_NOT_REPEAT = 0;
174 private static final int REPEATS_DAILY = 1;
175 private static final int REPEATS_EVERY_WEEKDAY = 2;
176 private static final int REPEATS_WEEKLY_ON_DAY = 3;
177 private static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
178 private static final int REPEATS_MONTHLY_ON_DAY = 5;
179 private static final int REPEATS_YEARLY = 6;
180 private static final int REPEATS_CUSTOM = 7;
182 private static final int MODIFY_UNINITIALIZED = 0;
183 private static final int MODIFY_SELECTED = 1;
184 private static final int MODIFY_ALL = 2;
185 private static final int MODIFY_ALL_FOLLOWING = 3;
187 private static final int DAY_IN_SECONDS = 24 * 60 * 60;
189 private int mFirstDayOfWeek; // cached in onCreate
191 private Cursor mEventCursor;
192 private Cursor mCalendarsCursor;
194 private Button mStartDateButton;
195 private Button mEndDateButton;
196 private Button mStartTimeButton;
197 private Button mEndTimeButton;
198 private Button mSaveButton;
199 private Button mDeleteButton;
200 private Button mDiscardButton;
201 private CheckBox mAllDayCheckBox;
202 private Spinner mCalendarsSpinner;
203 private Spinner mRepeatsSpinner;
204 private Spinner mAvailabilitySpinner;
205 private Spinner mVisibilitySpinner;
206 private TextView mTitleTextView;
207 private TextView mLocationTextView;
208 private TextView mDescriptionTextView;
209 private View mRemindersSeparator;
210 private LinearLayout mRemindersContainer;
211 private LinearLayout mExtraOptions;
212 private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
213 private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
214 MultiAutoCompleteTextView mAttendeesList;
215 private EmailAddressAdapter mAddressAdapter;
216 private String mOriginalAttendees = "";
218 private EventRecurrence mEventRecurrence = new EventRecurrence();
219 private String mRrule;
220 private boolean mCalendarsQueryComplete;
221 private boolean mSaveAfterQueryComplete;
222 private ProgressDialog mLoadingCalendarsDialog;
223 private AlertDialog mNoCalendarsDialog;
224 private ContentValues mInitialValues;
227 * If the repeating event is created on the phone and it hasn't been
228 * synced yet to the web server, then there is a bug where you can't
229 * delete or change an instance of the repeating event. This case
230 * can be detected with mSyncId. If mSyncId == null, then the repeating
231 * event has not been synced to the phone, in which case we won't allow
232 * the user to change one instance.
234 private String mSyncId;
236 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0);
237 private ArrayList<Integer> mReminderValues;
238 private ArrayList<String> mReminderLabels;
240 private Time mStartTime;
241 private Time mEndTime;
242 private int mModification = MODIFY_UNINITIALIZED;
243 private int mDefaultReminderMinutes;
245 private DeleteEventHelper mDeleteEventHelper;
246 private QueryHandler mQueryHandler;
248 /* This class is used to update the time buttons. */
249 private class TimeListener implements OnTimeSetListener {
252 public TimeListener(View view) {
256 public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
257 // Cache the member variables locally to avoid inner class overhead.
258 Time startTime = mStartTime;
259 Time endTime = mEndTime;
261 // Cache the start and end millis so that we limit the number
262 // of calls to normalize() and toMillis(), which are fairly
266 if (mView == mStartTimeButton) {
267 // The start time was changed.
268 int hourDuration = endTime.hour - startTime.hour;
269 int minuteDuration = endTime.minute - startTime.minute;
271 startTime.hour = hourOfDay;
272 startTime.minute = minute;
273 startMillis = startTime.normalize(true);
275 // Also update the end time to keep the duration constant.
276 endTime.hour = hourOfDay + hourDuration;
277 endTime.minute = minute + minuteDuration;
278 endMillis = endTime.normalize(true);
280 // The end time was changed.
281 startMillis = startTime.toMillis(true);
282 endTime.hour = hourOfDay;
283 endTime.minute = minute;
284 endMillis = endTime.normalize(true);
286 // Do not allow an event to have an end time before the start time.
287 if (endTime.before(startTime)) {
288 endTime.set(startTime);
289 endMillis = startMillis;
293 setDate(mEndDateButton, endMillis);
294 setTime(mStartTimeButton, startMillis);
295 setTime(mEndTimeButton, endMillis);
299 private class TimeClickListener implements View.OnClickListener {
302 public TimeClickListener(Time time) {
306 public void onClick(View v) {
307 new TimePickerDialog(EditEvent.this, new TimeListener(v),
308 mTime.hour, mTime.minute,
309 DateFormat.is24HourFormat(EditEvent.this)).show();
313 private class DateListener implements OnDateSetListener {
316 public DateListener(View view) {
320 public void onDateSet(DatePicker view, int year, int month, int monthDay) {
321 // Cache the member variables locally to avoid inner class overhead.
322 Time startTime = mStartTime;
323 Time endTime = mEndTime;
325 // Cache the start and end millis so that we limit the number
326 // of calls to normalize() and toMillis(), which are fairly
330 if (mView == mStartDateButton) {
331 // The start date was changed.
332 int yearDuration = endTime.year - startTime.year;
333 int monthDuration = endTime.month - startTime.month;
334 int monthDayDuration = endTime.monthDay - startTime.monthDay;
336 startTime.year = year;
337 startTime.month = month;
338 startTime.monthDay = monthDay;
339 startMillis = startTime.normalize(true);
341 // Also update the end date to keep the duration constant.
342 endTime.year = year + yearDuration;
343 endTime.month = month + monthDuration;
344 endTime.monthDay = monthDay + monthDayDuration;
345 endMillis = endTime.normalize(true);
347 // If the start date has changed then update the repeats.
350 // The end date was changed.
351 startMillis = startTime.toMillis(true);
353 endTime.month = month;
354 endTime.monthDay = monthDay;
355 endMillis = endTime.normalize(true);
357 // Do not allow an event to have an end time before the start time.
358 if (endTime.before(startTime)) {
359 endTime.set(startTime);
360 endMillis = startMillis;
364 setDate(mStartDateButton, startMillis);
365 setDate(mEndDateButton, endMillis);
366 setTime(mEndTimeButton, endMillis); // In case end time had to be reset
370 private class DateClickListener implements View.OnClickListener {
373 public DateClickListener(Time time) {
377 public void onClick(View v) {
378 new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year,
379 mTime.month, mTime.monthDay).show();
383 static private class CalendarsAdapter extends ResourceCursorAdapter {
384 public CalendarsAdapter(Context context, Cursor c) {
385 super(context, R.layout.calendars_item, c);
386 setDropDownViewResource(R.layout.calendars_dropdown_item);
390 public void bindView(View view, Context context, Cursor cursor) {
391 TextView name = (TextView) view.findViewById(R.id.calendar_name);
392 name.setText(cursor.getString(CALENDARS_INDEX_DISPLAY_NAME));
396 // This is called if the user clicks on one of the buttons: "Save",
397 // "Discard", or "Delete". This is also called if the user clicks
398 // on the "remove reminder" button.
399 public void onClick(View v) {
400 if (v == mSaveButton) {
407 if (v == mDeleteButton) {
408 long begin = mStartTime.toMillis(false /* use isDst */);
409 long end = mEndTime.toMillis(false /* use isDst */);
411 switch (mModification) {
412 case MODIFY_SELECTED:
413 which = DeleteEventHelper.DELETE_SELECTED;
415 case MODIFY_ALL_FOLLOWING:
416 which = DeleteEventHelper.DELETE_ALL_FOLLOWING;
419 which = DeleteEventHelper.DELETE_ALL;
422 mDeleteEventHelper.delete(begin, end, mEventCursor, which);
426 if (v == mDiscardButton) {
431 // This must be a click on one of the "remove reminder" buttons
432 LinearLayout reminderItem = (LinearLayout) v.getParent();
433 LinearLayout parent = (LinearLayout) reminderItem.getParent();
434 parent.removeView(reminderItem);
435 mReminderItems.remove(reminderItem);
436 updateRemindersVisibility();
439 // This is called if the user cancels a popup dialog. There are two
440 // dialogs: the "Loading calendars" dialog, and the "No calendars"
441 // dialog. The "Loading calendars" dialog is shown if there is a delay
442 // in loading the calendars (needed when creating an event) and the user
443 // tries to save the event before the calendars have finished loading.
444 // The "No calendars" dialog is shown if there are no syncable calendars.
445 public void onCancel(DialogInterface dialog) {
446 if (dialog == mLoadingCalendarsDialog) {
447 mSaveAfterQueryComplete = false;
448 } else if (dialog == mNoCalendarsDialog) {
453 // This is called if the user clicks on a dialog button.
454 public void onClick(DialogInterface dialog, int which) {
455 if (dialog == mNoCalendarsDialog) {
460 private class QueryHandler extends AsyncQueryHandler {
461 public QueryHandler(ContentResolver cr) {
466 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
467 // If the Activity is finishing, then close the cursor.
468 // Otherwise, use the new cursor in the adapter.
470 stopManagingCursor(cursor);
473 mCalendarsCursor = cursor;
474 startManagingCursor(cursor);
477 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
478 Window.PROGRESS_VISIBILITY_OFF);
480 // If there are no syncable calendars, then we cannot allow
481 // creating a new event.
482 if (cursor.getCount() == 0) {
483 // Cancel the "loading calendars" dialog if it exists
484 if (mSaveAfterQueryComplete) {
485 mLoadingCalendarsDialog.cancel();
488 // Create an error message for the user that, when clicked,
489 // will exit this activity without saving the event.
490 AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
491 builder.setTitle(R.string.no_syncable_calendars)
492 .setIcon(android.R.drawable.ic_dialog_alert)
493 .setMessage(R.string.no_calendars_found)
494 .setPositiveButton(android.R.string.ok, EditEvent.this)
495 .setOnCancelListener(EditEvent.this);
496 mNoCalendarsDialog = builder.show();
500 // Find user domain and set it to the validator
501 // TODO This won't work realiably if there are calendars in different domains
502 cursor.moveToFirst();
503 String ownEmail = cursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
504 if (ownEmail != null) {
505 int separator = ownEmail.lastIndexOf('@');
506 if (separator != -1 && ++separator < ownEmail.length()) {
507 mAttendeesList.setValidator(new Rfc822Validator(ownEmail
508 .substring(separator)));
512 // populate the calendars spinner
513 CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor);
514 mCalendarsSpinner.setAdapter(adapter);
515 mCalendarsQueryComplete = true;
516 if (mSaveAfterQueryComplete) {
517 mLoadingCalendarsDialog.cancel();
526 protected void onCreate(Bundle icicle) {
527 super.onCreate(icicle);
528 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
529 setContentView(R.layout.edit_event);
531 mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
533 mStartTime = new Time();
534 mEndTime = new Time();
536 Intent intent = getIntent();
537 mUri = intent.getData();
540 mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null);
541 if (mEventCursor == null || mEventCursor.getCount() == 0) {
542 // The cursor is empty. This can happen if the event was deleted.
548 long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
549 long end = intent.getLongExtra(EVENT_END_TIME, 0);
551 boolean allDay = false;
552 if (mEventCursor != null) {
553 // The event already exists so fetch the all-day status
554 mEventCursor.moveToFirst();
555 allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
556 String rrule = mEventCursor.getString(EVENT_INDEX_RRULE);
557 String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
558 long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
560 // Remember the initial values
561 mInitialValues = new ContentValues();
562 mInitialValues.put(EVENT_BEGIN_TIME, begin);
563 mInitialValues.put(EVENT_END_TIME, end);
564 mInitialValues.put(Events.ALL_DAY, allDay ? 1 : 0);
565 mInitialValues.put(Events.RRULE, rrule);
566 mInitialValues.put(Events.EVENT_TIMEZONE, timezone);
567 mInitialValues.put(Events.CALENDAR_ID, calendarId);
569 // We are creating a new event, so set the default from the
570 // intent (if specified).
571 allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false);
574 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
575 Window.PROGRESS_VISIBILITY_ON);
577 // Start a query in the background to read the list of calendars
578 mQueryHandler = new QueryHandler(getContentResolver());
579 mQueryHandler.startQuery(0, null, Calendars.CONTENT_URI, CALENDARS_PROJECTION,
580 CALENDARS_WHERE, null /* selection args */, null /* sort order */);
583 // If the event is all-day, read the times in UTC timezone
586 String tz = mStartTime.timezone;
587 mStartTime.timezone = Time.TIMEZONE_UTC;
588 mStartTime.set(begin);
589 mStartTime.timezone = tz;
591 // Calling normalize to calculate isDst
592 mStartTime.normalize(true);
594 mStartTime.set(begin);
600 String tz = mStartTime.timezone;
601 mEndTime.timezone = Time.TIMEZONE_UTC;
603 mEndTime.timezone = tz;
605 // Calling normalize to calculate isDst
606 mEndTime.normalize(true);
612 // cache all the widgets
613 mTitleTextView = (TextView) findViewById(R.id.title);
614 mLocationTextView = (TextView) findViewById(R.id.location);
615 mDescriptionTextView = (TextView) findViewById(R.id.description);
616 mStartDateButton = (Button) findViewById(R.id.start_date);
617 mEndDateButton = (Button) findViewById(R.id.end_date);
618 mStartTimeButton = (Button) findViewById(R.id.start_time);
619 mEndTimeButton = (Button) findViewById(R.id.end_time);
620 mAllDayCheckBox = (CheckBox) findViewById(R.id.is_all_day);
621 mCalendarsSpinner = (Spinner) findViewById(R.id.calendars);
622 mRepeatsSpinner = (Spinner) findViewById(R.id.repeats);
623 mAvailabilitySpinner = (Spinner) findViewById(R.id.availability);
624 mVisibilitySpinner = (Spinner) findViewById(R.id.visibility);
625 mRemindersSeparator = findViewById(R.id.reminders_separator);
626 mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
627 mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
629 mAddressAdapter = new EmailAddressAdapter(this);
630 mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees, R.string.hint_attendees);
632 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
633 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
635 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
637 long endMillis = mEndTime.normalize(true);
639 // Do not allow an event to have an end time before the start time.
640 if (mEndTime.before(mStartTime)) {
641 mEndTime.set(mStartTime);
642 endMillis = mEndTime.normalize(true);
644 setDate(mEndDateButton, endMillis);
645 setTime(mEndTimeButton, endMillis);
648 mStartTimeButton.setVisibility(View.GONE);
649 mEndTimeButton.setVisibility(View.GONE);
651 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
653 long endMillis = mEndTime.normalize(true);
654 setDate(mEndDateButton, endMillis);
655 setTime(mEndTimeButton, endMillis);
658 mStartTimeButton.setVisibility(View.VISIBLE);
659 mEndTimeButton.setVisibility(View.VISIBLE);
665 mAllDayCheckBox.setChecked(true);
667 mAllDayCheckBox.setChecked(false);
670 mSaveButton = (Button) findViewById(R.id.save);
671 mSaveButton.setOnClickListener(this);
673 mDeleteButton = (Button) findViewById(R.id.delete);
674 mDeleteButton.setOnClickListener(this);
676 mDiscardButton = (Button) findViewById(R.id.discard);
677 mDiscardButton.setOnClickListener(this);
679 // Initialize the reminder values array.
680 Resources r = getResources();
681 String[] strings = r.getStringArray(R.array.reminder_minutes_values);
682 int size = strings.length;
683 ArrayList<Integer> list = new ArrayList<Integer>(size);
684 for (int i = 0 ; i < size ; i++) {
685 list.add(Integer.parseInt(strings[i]));
687 mReminderValues = list;
688 String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
689 mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
691 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
692 String durationString =
693 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
694 mDefaultReminderMinutes = Integer.parseInt(durationString);
696 long eventId = (mEventCursor == null) ? -1 : mEventCursor.getLong(EVENT_INDEX_ID);
697 ContentResolver cr = getContentResolver();
700 boolean hasAlarm = (mEventCursor != null)
701 && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0);
703 Uri uri = Reminders.CONTENT_URI;
704 String where = String.format(REMINDERS_WHERE, eventId);
705 Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
707 // First pass: collect all the custom reminder minutes (e.g.,
708 // a reminder of 8 minutes) into a global list.
709 while (reminderCursor.moveToNext()) {
710 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
711 EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
714 // Second pass: create the reminder spinners
715 reminderCursor.moveToPosition(-1);
716 while (reminderCursor.moveToNext()) {
717 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
718 mOriginalMinutes.add(minutes);
719 EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
720 mReminderLabels, minutes);
723 reminderCursor.close();
726 updateRemindersVisibility();
728 // Setup the + Add Reminder Button
729 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
730 public void onClick(View v) {
734 ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
735 reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
737 mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
741 Uri uri = Attendees.CONTENT_URI;
742 String[] whereArgs = {Long.toString(eventId)};
743 Cursor attendeeCursor = cr.query(uri, ATTENDEES_PROJECTION, ATTENDEES_WHERE, whereArgs,
746 StringBuilder b = new StringBuilder();
747 while (attendeeCursor.moveToNext()) {
748 String name = attendeeCursor.getString(ATTENDEES_INDEX_NAME);
749 String email = attendeeCursor.getString(ATTENDEES_INDEX_EMAIL);
751 if (name != null && name.length() > 0 && !name.equals(email)) {
752 b.append('"').append(name).append("\" ");
754 b.append('<').append(email).append(">, ");
757 if (b.length() > 0) {
758 mOriginalAttendees = b.toString();
759 mAttendeesList.setText(mOriginalAttendees);
762 attendeeCursor.close();
765 if (mEventCursor == null) {
766 // Allow the intent to specify the fields in the event.
767 // This will allow other apps to create events easily.
768 initFromIntent(intent);
772 private Rfc822Token[] getAddressesFromList(MultiAutoCompleteTextView list) {
773 list.clearComposingText();
774 return Rfc822Tokenizer.tokenize(list.getText());
777 // From com.google.android.gm.ComposeActivity
778 private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res, int hintId) {
779 MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res);
780 list.setAdapter(mAddressAdapter);
781 list.setTokenizer(new Rfc822Tokenizer());
783 // NOTE: assumes no other filters are set
784 list.setFilters(sRecipientFilters);
790 * From com.google.android.gm.ComposeActivity
791 * Implements special address cleanup rules:
792 * The first space key entry following an "@" symbol that is followed by any combination
793 * of letters and symbols, including one+ dots and zero commas, should insert an extra
794 * comma (followed by the space).
796 private static InputFilter[] sRecipientFilters = new InputFilter[] { new InputFilter() {
798 public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
799 int dstart, int dend) {
801 // quick check - did they enter a single space?
802 if (end-start != 1 || source.charAt(start) != ' ') {
806 // determine if the characters before the new space fit the pattern
807 // follow backwards and see if we find a comma, dot, or @
808 int scanBack = dstart;
809 boolean dotFound = false;
810 while (scanBack > 0) {
811 char c = dest.charAt(--scanBack);
814 dotFound = true; // one or more dots are req'd
822 // we have found a comma-insert case. now just do it
823 // in the least expensive way we can.
824 if (source instanceof Spanned) {
825 SpannableStringBuilder sb = new SpannableStringBuilder(",");
836 // no termination cases were found, so don't edit the input
841 private void initFromIntent(Intent intent) {
842 String title = intent.getStringExtra(Events.TITLE);
844 mTitleTextView.setText(title);
847 String location = intent.getStringExtra(Events.EVENT_LOCATION);
848 if (location != null) {
849 mLocationTextView.setText(location);
852 String description = intent.getStringExtra(Events.DESCRIPTION);
853 if (description != null) {
854 mDescriptionTextView.setText(description);
857 int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
858 if (availability != -1) {
859 mAvailabilitySpinner.setSelection(availability);
862 int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
863 if (visibility != -1) {
864 mVisibilitySpinner.setSelection(visibility);
867 String rrule = intent.getStringExtra(Events.RRULE);
870 mEventRecurrence.parse(rrule);
875 protected void onResume() {
879 if (mEventCursor == null || mEventCursor.getCount() == 0) {
880 // The cursor is empty. This can happen if the event was deleted.
886 if (mEventCursor != null) {
887 Cursor cursor = mEventCursor;
888 cursor.moveToFirst();
890 mRrule = cursor.getString(EVENT_INDEX_RRULE);
891 String title = cursor.getString(EVENT_INDEX_TITLE);
892 String description = cursor.getString(EVENT_INDEX_DESCRIPTION);
893 String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
894 int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY);
895 int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY);
896 if (visibility > 0) {
897 // For now we the array contains the values 0, 2, and 3. We subtract one to match.
901 if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) {
902 // If this event has not been synced, then don't allow deleting
903 // or changing a single instance.
904 mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
905 mEventRecurrence.parse(mRrule);
907 // If we haven't synced this repeating event yet, then don't
908 // allow the user to change just one instance.
910 CharSequence[] items;
911 if (mSyncId == null) {
912 items = new CharSequence[2];
914 items = new CharSequence[3];
915 items[itemIndex++] = getText(R.string.modify_event);
917 items[itemIndex++] = getText(R.string.modify_all);
918 items[itemIndex++] = getText(R.string.modify_all_following);
920 // Display the modification dialog.
921 new AlertDialog.Builder(this)
922 .setOnCancelListener(new OnCancelListener() {
923 public void onCancel(DialogInterface dialog) {
927 .setTitle(R.string.edit_event_label)
928 .setItems(items, new OnClickListener() {
929 public void onClick(DialogInterface dialog, int which) {
932 (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED;
933 } else if (which == 1) {
935 (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL;
936 } else if (which == 2) {
937 mModification = MODIFY_ALL_FOLLOWING;
940 // If we are modifying all the events in a
941 // series then disable and ignore the date.
942 if (mModification == MODIFY_ALL) {
943 mStartDateButton.setEnabled(false);
944 mEndDateButton.setEnabled(false);
945 } else if (mModification == MODIFY_SELECTED) {
946 mRepeatsSpinner.setEnabled(false);
953 mTitleTextView.setText(title);
954 mLocationTextView.setText(location);
955 mDescriptionTextView.setText(description);
956 mAvailabilitySpinner.setSelection(availability);
957 mVisibilitySpinner.setSelection(visibility);
959 // This is an existing event so hide the calendar spinner
960 // since we can't change the calendar.
961 View calendarGroup = findViewById(R.id.calendar_group);
962 calendarGroup.setVisibility(View.GONE);
965 if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) {
966 mStartTime.setToNow();
968 // Round the time to the nearest half hour.
969 mStartTime.second = 0;
970 int minute = mStartTime.minute;
971 if (minute > 0 && minute <= 30) {
972 mStartTime.minute = 30;
974 mStartTime.minute = 0;
975 mStartTime.hour += 1;
978 long startMillis = mStartTime.normalize(true /* ignore isDst */);
979 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
982 // New event - set the default reminder
983 if (mDefaultReminderMinutes != 0) {
984 addReminder(this, this, mReminderItems, mReminderValues,
985 mReminderLabels, mDefaultReminderMinutes);
988 // Hide delete button
989 mDeleteButton.setVisibility(View.GONE);
992 updateRemindersVisibility();
998 public boolean onCreateOptionsMenu(Menu menu) {
1000 item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
1001 R.string.add_new_reminder);
1002 item.setIcon(R.drawable.ic_menu_reminder);
1003 item.setAlphabeticShortcut('r');
1005 item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
1006 R.string.edit_event_show_extra_options);
1007 item.setIcon(R.drawable.ic_menu_show_list);
1008 item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
1009 R.string.edit_event_hide_extra_options);
1010 item.setIcon(R.drawable.ic_menu_show_list);
1012 return super.onCreateOptionsMenu(menu);
1016 public boolean onPrepareOptionsMenu(Menu menu) {
1017 if (mReminderItems.size() < MAX_REMINDERS) {
1018 menu.setGroupVisible(MENU_GROUP_REMINDER, true);
1019 menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
1021 menu.setGroupVisible(MENU_GROUP_REMINDER, false);
1022 menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
1025 if (mExtraOptions.getVisibility() == View.VISIBLE) {
1026 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
1027 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
1029 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
1030 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
1033 return super.onPrepareOptionsMenu(menu);
1036 private void addReminder() {
1037 // TODO: when adding a new reminder, make it different from the
1038 // last one in the list (if any).
1039 if (mDefaultReminderMinutes == 0) {
1040 addReminder(this, this, mReminderItems, mReminderValues,
1041 mReminderLabels, 10 /* minutes */);
1043 addReminder(this, this, mReminderItems, mReminderValues,
1044 mReminderLabels, mDefaultReminderMinutes);
1046 updateRemindersVisibility();
1050 public boolean onOptionsItemSelected(MenuItem item) {
1051 switch (item.getItemId()) {
1052 case MENU_ADD_REMINDER:
1055 case MENU_SHOW_EXTRA_OPTIONS:
1056 mExtraOptions.setVisibility(View.VISIBLE);
1058 case MENU_HIDE_EXTRA_OPTIONS:
1059 mExtraOptions.setVisibility(View.GONE);
1062 return super.onOptionsItemSelected(item);
1066 public boolean onKeyDown(int keyCode, KeyEvent event) {
1068 case KeyEvent.KEYCODE_BACK:
1069 // If we are creating a new event, do not create it if the
1070 // title, location and description are all empty, in order to
1071 // prevent accidental "no subject" event creations.
1072 if (mUri != null || !isEmpty()) {
1074 // We cannot exit this activity because the calendars
1075 // are still loading.
1082 return super.onKeyDown(keyCode, event);
1085 private void populateWhen() {
1086 long startMillis = mStartTime.toMillis(false /* use isDst */);
1087 long endMillis = mEndTime.toMillis(false /* use isDst */);
1088 setDate(mStartDateButton, startMillis);
1089 setDate(mEndDateButton, endMillis);
1091 setTime(mStartTimeButton, startMillis);
1092 setTime(mEndTimeButton, endMillis);
1094 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
1095 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
1097 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
1098 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
1101 private void populateRepeats() {
1102 Time time = mStartTime;
1103 Resources r = getResources();
1104 int resource = android.R.layout.simple_spinner_item;
1106 String[] days = new String[] {
1107 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
1108 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
1109 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
1110 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
1111 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
1112 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
1113 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
1115 String[] ordinals = r.getStringArray(R.array.ordinal_labels);
1117 // Only display "Custom" in the spinner if the device does not support the
1118 // recurrence functionality of the event. Only display every weekday if
1119 // the event starts on a weekday.
1120 boolean isCustomRecurrence = isCustomRecurrence();
1121 boolean isWeekdayEvent = isWeekdayEvent();
1123 ArrayList<String> repeatArray = new ArrayList<String>(0);
1124 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
1126 repeatArray.add(r.getString(R.string.does_not_repeat));
1127 recurrenceIndexes.add(DOES_NOT_REPEAT);
1129 repeatArray.add(r.getString(R.string.daily));
1130 recurrenceIndexes.add(REPEATS_DAILY);
1132 if (isWeekdayEvent) {
1133 repeatArray.add(r.getString(R.string.every_weekday));
1134 recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
1137 String format = r.getString(R.string.weekly);
1138 repeatArray.add(String.format(format, time.format("%A")));
1139 recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
1141 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
1142 int dayNumber = (time.monthDay - 1) / 7;
1143 format = r.getString(R.string.monthly_on_day_count);
1144 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
1145 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
1147 format = r.getString(R.string.monthly_on_day);
1148 repeatArray.add(String.format(format, time.monthDay));
1149 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
1151 long when = time.toMillis(false);
1152 format = r.getString(R.string.yearly);
1154 if (DateFormat.is24HourFormat(this)) {
1155 flags |= DateUtils.FORMAT_24HOUR;
1157 repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
1158 recurrenceIndexes.add(REPEATS_YEARLY);
1160 if (isCustomRecurrence) {
1161 repeatArray.add(r.getString(R.string.custom));
1162 recurrenceIndexes.add(REPEATS_CUSTOM);
1164 mRecurrenceIndexes = recurrenceIndexes;
1166 int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
1167 if (mRrule != null) {
1168 if (isCustomRecurrence) {
1169 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
1171 switch (mEventRecurrence.freq) {
1172 case EventRecurrence.DAILY:
1173 position = recurrenceIndexes.indexOf(REPEATS_DAILY);
1175 case EventRecurrence.WEEKLY:
1176 if (mEventRecurrence.repeatsOnEveryWeekDay()) {
1177 position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
1179 position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
1182 case EventRecurrence.MONTHLY:
1183 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1184 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
1186 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
1189 case EventRecurrence.YEARLY:
1190 position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
1195 ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
1196 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1197 mRepeatsSpinner.setAdapter(adapter);
1198 mRepeatsSpinner.setSelection(position);
1201 // Adds a reminder to the displayed list of reminders.
1202 // Returns true if successfully added reminder, false if no reminders can
1204 static boolean addReminder(Activity activity, View.OnClickListener listener,
1205 ArrayList<LinearLayout> items, ArrayList<Integer> values,
1206 ArrayList<String> labels, int minutes) {
1208 if (items.size() >= MAX_REMINDERS) {
1212 LayoutInflater inflater = activity.getLayoutInflater();
1213 LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
1214 LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
1215 parent.addView(reminderItem);
1217 Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
1218 Resources res = activity.getResources();
1219 spinner.setPrompt(res.getString(R.string.reminders_label));
1220 int resource = android.R.layout.simple_spinner_item;
1221 ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
1222 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1223 spinner.setAdapter(adapter);
1225 ImageButton reminderRemoveButton;
1226 reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
1227 reminderRemoveButton.setOnClickListener(listener);
1229 int index = findMinutesInReminderList(values, minutes);
1230 spinner.setSelection(index);
1231 items.add(reminderItem);
1236 static void addMinutesToList(Context context, ArrayList<Integer> values,
1237 ArrayList<String> labels, int minutes) {
1238 int index = values.indexOf(minutes);
1243 // The requested "minutes" does not exist in the list, so insert it
1246 String label = constructReminderLabel(context, minutes, false);
1247 int len = values.size();
1248 for (int i = 0; i < len; i++) {
1249 if (minutes < values.get(i)) {
1250 values.add(i, minutes);
1251 labels.add(i, label);
1256 values.add(minutes);
1257 labels.add(len, label);
1261 * Finds the index of the given "minutes" in the "values" list.
1263 * @param values the list of minutes corresponding to the spinner choices
1264 * @param minutes the minutes to search for in the values list
1265 * @return the index of "minutes" in the "values" list
1267 private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
1268 int index = values.indexOf(minutes);
1270 // This should never happen.
1271 Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
1277 // Constructs a label given an arbitrary number of minutes. For example,
1278 // if the given minutes is 63, then this returns the string "63 minutes".
1279 // As another example, if the given minutes is 120, then this returns
1281 static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
1282 Resources resources = context.getResources();
1285 if (minutes % 60 != 0) {
1288 resId = R.plurals.Nmins;
1290 resId = R.plurals.Nminutes;
1292 } else if (minutes % (24 * 60) != 0) {
1293 value = minutes / 60;
1294 resId = R.plurals.Nhours;
1296 value = minutes / ( 24 * 60);
1297 resId = R.plurals.Ndays;
1300 String format = resources.getQuantityString(resId, value);
1301 return String.format(format, value);
1304 private void updateRemindersVisibility() {
1305 if (mReminderItems.size() == 0) {
1306 mRemindersSeparator.setVisibility(View.GONE);
1307 mRemindersContainer.setVisibility(View.GONE);
1309 mRemindersSeparator.setVisibility(View.VISIBLE);
1310 mRemindersContainer.setVisibility(View.VISIBLE);
1314 private void setDate(TextView view, long millis) {
1315 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
1316 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
1317 DateUtils.FORMAT_ABBREV_WEEKDAY;
1318 view.setText(DateUtils.formatDateTime(this, millis, flags));
1321 private void setTime(TextView view, long millis) {
1322 int flags = DateUtils.FORMAT_SHOW_TIME;
1323 if (DateFormat.is24HourFormat(this)) {
1324 flags |= DateUtils.FORMAT_24HOUR;
1326 view.setText(DateUtils.formatDateTime(this, millis, flags));
1329 // Saves the event. Returns true if it is okay to exit this activity.
1330 private boolean save() {
1331 boolean forceSaveReminders = false;
1333 // If we are creating a new event, then make sure we wait until the
1334 // query to fetch the list of calendars has finished.
1335 if (mEventCursor == null) {
1336 if (!mCalendarsQueryComplete) {
1337 // Wait for the calendars query to finish.
1338 if (mLoadingCalendarsDialog == null) {
1339 // Create the progress dialog
1340 mLoadingCalendarsDialog = ProgressDialog.show(this,
1341 getText(R.string.loading_calendars_title),
1342 getText(R.string.loading_calendars_message),
1344 mSaveAfterQueryComplete = true;
1349 // Avoid creating a new event if the calendars cursor is empty. This
1350 // shouldn't ever happen since the setup wizard should ensure the user
1352 if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0) {
1353 Log.w("Cal", "The calendars table does not contain any calendars."
1354 + " New event was not created.");
1357 Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
1359 Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
1362 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1363 int eventIdIndex = -1;
1365 ContentValues values = getContentValuesFromUi();
1368 // Update the "hasAlarm" field for the event
1369 ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
1371 int len = reminderMinutes.size();
1372 values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
1374 // For recurring events, we must make sure that we use duration rather
1377 // Create new event with new contents
1378 addRecurrenceRule(values);
1379 eventIdIndex = ops.size();
1380 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1382 forceSaveReminders = true;
1384 } else if (mRrule == null) {
1385 // Modify contents of a non-repeating event
1386 addRecurrenceRule(values);
1387 checkTimeDependentFields(values);
1388 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1390 } else if (mInitialValues.getAsString(Events.RRULE) == null) {
1391 // This event was changed from a non-repeating event to a
1393 addRecurrenceRule(values);
1394 values.remove(Events.DTEND);
1395 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1397 } else if (mModification == MODIFY_SELECTED) {
1398 // Modify contents of the current instance of repeating event
1400 // Create a recurrence exception
1401 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1402 values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
1403 values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
1404 boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1405 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
1407 eventIdIndex = ops.size();
1408 Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
1410 forceSaveReminders = true;
1412 } else if (mModification == MODIFY_ALL_FOLLOWING) {
1413 // Modify this instance and all future instances of repeating event
1414 addRecurrenceRule(values);
1416 if (mRrule == null) {
1417 // We've changed a recurring event to a non-recurring event.
1418 // If the event we are editing is the first in the series,
1419 // then delete the whole series. Otherwise, update the series
1420 // to end at the new start time.
1421 if (isFirstEventInSeries()) {
1422 ops.add(ContentProviderOperation.newDelete(uri).build());
1424 // Update the current repeating event to end at the new
1426 updatePastEvents(ops, uri);
1428 eventIdIndex = ops.size();
1429 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1432 if (isFirstEventInSeries()) {
1433 checkTimeDependentFields(values);
1434 values.remove(Events.DTEND);
1435 Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
1438 // Update the current repeating event to end at the new
1440 updatePastEvents(ops, uri);
1442 // Create a new event with the user-modified fields
1443 values.remove(Events.DTEND);
1444 eventIdIndex = ops.size();
1445 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
1449 forceSaveReminders = true;
1451 } else if (mModification == MODIFY_ALL) {
1453 // Modify all instances of repeating event
1454 addRecurrenceRule(values);
1456 if (mRrule == null) {
1457 // We've changed a recurring event to a non-recurring event.
1458 // Delete the whole series and replace it with a new
1459 // non-recurring event.
1460 ops.add(ContentProviderOperation.newDelete(uri).build());
1462 eventIdIndex = ops.size();
1463 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
1465 forceSaveReminders = true;
1467 checkTimeDependentFields(values);
1468 values.remove(Events.DTEND);
1469 ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
1473 if (eventIdIndex != -1) {
1474 saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
1475 forceSaveReminders);
1476 } else if (uri != null) {
1477 long eventId = ContentUris.parseId(uri);
1478 saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
1479 forceSaveReminders);
1482 if (eventIdIndex != -1 || uri != null) {
1483 Editable attendeesText = mAttendeesList.getText();
1484 // Hit the content provider only if the user has changed it
1485 if (!mOriginalAttendees.equals(attendeesText.toString())) {
1486 // TODO we could do a diff and modify the rows only as needed
1487 // Delete all the existing attendees for this event
1488 Builder b = ContentProviderOperation.newDelete(Attendees.CONTENT_URI);
1491 if (eventIdIndex == -1) {
1492 eventId = ContentUris.parseId(uri);
1493 String[] args = new String[] {
1494 Long.toString(eventId)
1496 b.withSelection(ATTENDEES_WHERE, args);
1498 // Delete all the existing reminders for this event
1499 b.withSelection(ATTENDEES_WHERE, new String[1]);
1500 b.withSelectionBackReference(0, eventIdIndex);
1504 if (attendeesText.length() > 0) {
1505 Rfc822Token[] attendees = getAddressesFromList(mAttendeesList);
1506 // Insert the attendees
1507 for (Rfc822Token attendee : attendees) {
1509 values.put(Attendees.ATTENDEE_NAME, attendee.getName());
1510 values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
1511 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
1512 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
1513 values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
1515 if (eventIdIndex != -1) {
1516 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1517 .withValues(values);
1518 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1520 values.put(Attendees.EVENT_ID, eventId);
1521 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
1522 .withValues(values);
1531 // TODO Move this to background thread
1532 ContentProviderResult[] results =
1533 getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
1535 for (int i = 0; i < results.length; i++) {
1536 Log.v(TAG, "results = " + results[i].toString());
1539 } catch (RemoteException e) {
1540 // TODO Auto-generated catch block
1541 e.printStackTrace();
1542 } catch (OperationApplicationException e) {
1543 // TODO Auto-generated catch block
1544 e.printStackTrace();
1550 private boolean isFirstEventInSeries() {
1551 int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
1552 long start = mEventCursor.getLong(dtStart);
1553 return start == mStartTime.toMillis(true);
1556 private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) {
1557 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1558 String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
1559 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1560 String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
1561 mEventRecurrence.parse(oldRrule);
1563 Time untilTime = new Time();
1564 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1565 ContentValues oldValues = new ContentValues();
1567 // The "until" time must be in UTC time in order for Google calendar
1568 // to display it properly. For all-day events, the "until" time string
1569 // must include just the date field, and not the time field. The
1570 // repeating events repeat up to and including the "until" time.
1571 untilTime.timezone = Time.TIMEZONE_UTC;
1573 // Subtract one second from the old begin time to get the new
1575 untilTime.set(begin - 1000); // subtract one second (1000 millis)
1578 untilTime.minute = 0;
1579 untilTime.second = 0;
1580 untilTime.allDay = true;
1581 untilTime.normalize(false);
1583 // For all-day events, the duration must be in days, not seconds.
1584 // Otherwise, Google Calendar will (mistakenly) change this event
1585 // into a non-all-day event.
1586 int len = oldDuration.length();
1587 if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
1588 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
1589 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1590 oldDuration = "P" + days + "D";
1593 mEventRecurrence.until = untilTime.format2445();
1595 oldValues.put(Events.DTSTART, oldStartMillis);
1596 oldValues.put(Events.DURATION, oldDuration);
1597 oldValues.put(Events.RRULE, mEventRecurrence.toString());
1598 Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
1602 private void checkTimeDependentFields(ContentValues values) {
1603 long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1604 long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
1605 boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1606 String oldRrule = mInitialValues.getAsString(Events.RRULE);
1607 String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
1609 long newBegin = values.getAsLong(Events.DTSTART);
1610 long newEnd = values.getAsLong(Events.DTEND);
1611 boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
1612 String newRrule = values.getAsString(Events.RRULE);
1613 String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1615 // If none of the time-dependent fields changed, then remove them.
1616 if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
1617 && TextUtils.equals(oldRrule, newRrule)
1618 && TextUtils.equals(oldTimezone, newTimezone)) {
1619 values.remove(Events.DTSTART);
1620 values.remove(Events.DTEND);
1621 values.remove(Events.DURATION);
1622 values.remove(Events.ALL_DAY);
1623 values.remove(Events.RRULE);
1624 values.remove(Events.EVENT_TIMEZONE);
1628 if (oldRrule == null || newRrule == null) {
1632 // If we are modifying all events then we need to set DTSTART to the
1633 // start time of the first event in the series, not the current
1634 // date and time. If the start time of the event was changed
1635 // (from, say, 3pm to 4pm), then we want to add the time difference
1636 // to the start time of the first event in the series (the DTSTART
1637 // value). If we are modifying one instance or all following instances,
1638 // then we leave the DTSTART field alone.
1639 if (mModification == MODIFY_ALL) {
1640 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1641 if (oldBegin != newBegin) {
1642 // The user changed the start time of this event
1643 long offset = newBegin - oldBegin;
1644 oldStartMillis += offset;
1646 values.put(Events.DTSTART, oldStartMillis);
1650 static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
1651 ArrayList<Integer> reminderValues) {
1652 int len = reminderItems.size();
1653 ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
1654 for (int index = 0; index < len; index++) {
1655 LinearLayout layout = reminderItems.get(index);
1656 Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
1657 int minutes = reminderValues.get(spinner.getSelectedItemPosition());
1658 reminderMinutes.add(minutes);
1660 return reminderMinutes;
1664 * Saves the reminders, if they changed. Returns true if the database
1667 * @param ops the array of ContentProviderOperations
1668 * @param eventId the id of the event whose reminders are being updated
1669 * @param reminderMinutes the array of reminders set by the user
1670 * @param originalMinutes the original array of reminders
1671 * @param forceSave if true, then save the reminders even if they didn't
1673 * @return true if the database was updated
1675 static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
1676 ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
1677 boolean forceSave) {
1678 // If the reminders have not changed, then don't update the database
1679 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1683 // Delete all the existing reminders for this event
1684 String where = Reminders.EVENT_ID + "=?";
1685 String[] args = new String[] { Long.toString(eventId) };
1686 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1687 b.withSelection(where, args);
1690 ContentValues values = new ContentValues();
1691 int len = reminderMinutes.size();
1693 // Insert the new reminders, if any
1694 for (int i = 0; i < len; i++) {
1695 int minutes = reminderMinutes.get(i);
1698 values.put(Reminders.MINUTES, minutes);
1699 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1700 values.put(Reminders.EVENT_ID, eventId);
1701 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1707 static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
1708 int eventIdIndex, ArrayList<Integer> reminderMinutes,
1709 ArrayList<Integer> originalMinutes, boolean forceSave) {
1710 // If the reminders have not changed, then don't update the database
1711 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1715 // Delete all the existing reminders for this event
1716 Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
1717 b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
1718 b.withSelectionBackReference(0, eventIdIndex);
1721 ContentValues values = new ContentValues();
1722 int len = reminderMinutes.size();
1724 // Insert the new reminders, if any
1725 for (int i = 0; i < len; i++) {
1726 int minutes = reminderMinutes.get(i);
1729 values.put(Reminders.MINUTES, minutes);
1730 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1731 b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
1732 b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
1738 private void addRecurrenceRule(ContentValues values) {
1739 updateRecurrenceRule();
1741 if (mRrule == null) {
1745 values.put(Events.RRULE, mRrule);
1746 long end = mEndTime.toMillis(true /* ignore dst */);
1747 long start = mStartTime.toMillis(true /* ignore dst */);
1750 boolean isAllDay = mAllDayCheckBox.isChecked();
1752 long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
1753 duration = "P" + days + "D";
1755 long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
1756 duration = "P" + seconds + "S";
1758 values.put(Events.DURATION, duration);
1761 private void updateRecurrenceRule() {
1762 int position = mRepeatsSpinner.getSelectedItemPosition();
1763 int selection = mRecurrenceIndexes.get(position);
1765 if (selection == DOES_NOT_REPEAT) {
1768 } else if (selection == REPEATS_CUSTOM) {
1769 // Keep custom recurrence as before.
1771 } else if (selection == REPEATS_DAILY) {
1772 mEventRecurrence.freq = EventRecurrence.DAILY;
1773 } else if (selection == REPEATS_EVERY_WEEKDAY) {
1774 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1776 int[] byday = new int[dayCount];
1777 int[] bydayNum = new int[dayCount];
1779 byday[0] = EventRecurrence.MO;
1780 byday[1] = EventRecurrence.TU;
1781 byday[2] = EventRecurrence.WE;
1782 byday[3] = EventRecurrence.TH;
1783 byday[4] = EventRecurrence.FR;
1784 for (int day = 0; day < dayCount; day++) {
1788 mEventRecurrence.byday = byday;
1789 mEventRecurrence.bydayNum = bydayNum;
1790 mEventRecurrence.bydayCount = dayCount;
1791 } else if (selection == REPEATS_WEEKLY_ON_DAY) {
1792 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1793 int[] days = new int[1];
1795 int[] dayNum = new int[dayCount];
1797 days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1798 // not sure why this needs to be zero, but set it for now.
1801 mEventRecurrence.byday = days;
1802 mEventRecurrence.bydayNum = dayNum;
1803 mEventRecurrence.bydayCount = dayCount;
1804 } else if (selection == REPEATS_MONTHLY_ON_DAY) {
1805 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1806 mEventRecurrence.bydayCount = 0;
1807 mEventRecurrence.bymonthdayCount = 1;
1808 int[] bymonthday = new int[1];
1809 bymonthday[0] = mStartTime.monthDay;
1810 mEventRecurrence.bymonthday = bymonthday;
1811 } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
1812 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1813 mEventRecurrence.bydayCount = 1;
1814 mEventRecurrence.bymonthdayCount = 0;
1816 int[] byday = new int[1];
1817 int[] bydayNum = new int[1];
1818 // Compute the week number (for example, the "2nd" Monday)
1819 int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
1820 if (dayCount == 5) {
1823 bydayNum[0] = dayCount;
1824 byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1825 mEventRecurrence.byday = byday;
1826 mEventRecurrence.bydayNum = bydayNum;
1827 } else if (selection == REPEATS_YEARLY) {
1828 mEventRecurrence.freq = EventRecurrence.YEARLY;
1831 // Set the week start day.
1832 mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
1833 mRrule = mEventRecurrence.toString();
1836 private ContentValues getContentValuesFromUi() {
1837 String title = mTitleTextView.getText().toString();
1838 boolean isAllDay = mAllDayCheckBox.isChecked();
1839 String location = mLocationTextView.getText().toString();
1840 String description = mDescriptionTextView.getText().toString();
1842 ContentValues values = new ContentValues();
1844 String timezone = null;
1849 // Reset start and end time, increment the monthDay by 1, and set
1850 // the timezone to UTC, as required for all-day events.
1851 timezone = Time.TIMEZONE_UTC;
1852 mStartTime.hour = 0;
1853 mStartTime.minute = 0;
1854 mStartTime.second = 0;
1855 mStartTime.timezone = timezone;
1856 startMillis = mStartTime.normalize(true);
1859 mEndTime.minute = 0;
1860 mEndTime.second = 0;
1861 mEndTime.monthDay++;
1862 mEndTime.timezone = timezone;
1863 endMillis = mEndTime.normalize(true);
1865 if (mEventCursor == null) {
1866 // This is a new event
1867 calendarId = mCalendarsSpinner.getSelectedItemId();
1869 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1872 startMillis = mStartTime.toMillis(true);
1873 endMillis = mEndTime.toMillis(true);
1874 if (mEventCursor != null) {
1875 // This is an existing event
1876 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
1878 // The timezone might be null if we are changing an existing
1879 // all-day event to a non-all-day event. We need to assign
1880 // a timezone to the non-all-day event.
1881 if (TextUtils.isEmpty(timezone)) {
1882 timezone = TimeZone.getDefault().getID();
1884 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1886 // This is a new event
1887 calendarId = mCalendarsSpinner.getSelectedItemId();
1889 // The timezone for a new event is the currently displayed
1890 // timezone, NOT the timezone of the containing calendar.
1891 timezone = TimeZone.getDefault().getID();
1895 values.put(Events.CALENDAR_ID, calendarId);
1896 values.put(Events.EVENT_TIMEZONE, timezone);
1897 values.put(Events.TITLE, title);
1898 values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
1899 values.put(Events.DTSTART, startMillis);
1900 values.put(Events.DTEND, endMillis);
1901 values.put(Events.DESCRIPTION, description);
1902 values.put(Events.EVENT_LOCATION, location);
1903 values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
1905 int visibility = mVisibilitySpinner.getSelectedItemPosition();
1906 if (visibility > 0) {
1907 // For now we the array contains the values 0, 2, and 3. We add one to match.
1910 values.put(Events.VISIBILITY, visibility);
1915 private boolean isEmpty() {
1916 String title = mTitleTextView.getText().toString();
1917 if (title.length() > 0) {
1921 String location = mLocationTextView.getText().toString();
1922 if (location.length() > 0) {
1926 String description = mDescriptionTextView.getText().toString();
1927 if (description.length() > 0) {
1934 private boolean isCustomRecurrence() {
1936 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
1940 if (mEventRecurrence.freq == 0) {
1944 switch (mEventRecurrence.freq) {
1945 case EventRecurrence.DAILY:
1947 case EventRecurrence.WEEKLY:
1948 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
1950 } else if (mEventRecurrence.bydayCount == 1) {
1954 case EventRecurrence.MONTHLY:
1955 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1957 } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
1961 case EventRecurrence.YEARLY:
1968 private boolean isWeekdayEvent() {
1969 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {