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;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.DatePickerDialog;
24 import android.app.ProgressDialog;
25 import android.app.TimePickerDialog;
26 import android.app.DatePickerDialog.OnDateSetListener;
27 import android.app.TimePickerDialog.OnTimeSetListener;
28 import android.content.AsyncQueryHandler;
29 import android.content.ContentResolver;
30 import android.content.ContentUris;
31 import android.content.ContentValues;
32 import android.content.Context;
33 import android.content.DialogInterface;
34 import android.content.Intent;
35 import android.content.SharedPreferences;
36 import android.content.DialogInterface.OnCancelListener;
37 import android.content.DialogInterface.OnClickListener;
38 import android.content.res.Resources;
39 import android.database.Cursor;
40 import android.net.Uri;
41 import android.os.Bundle;
42 import android.pim.EventRecurrence;
43 import android.preference.PreferenceManager;
44 import android.provider.Calendar.Calendars;
45 import android.provider.Calendar.Events;
46 import android.provider.Calendar.Reminders;
47 import android.text.TextUtils;
48 import android.text.format.DateFormat;
49 import android.text.format.DateUtils;
50 import android.text.format.Time;
51 import android.util.Log;
52 import android.view.KeyEvent;
53 import android.view.LayoutInflater;
54 import android.view.Menu;
55 import android.view.MenuItem;
56 import android.view.View;
57 import android.view.Window;
58 import android.widget.ArrayAdapter;
59 import android.widget.Button;
60 import android.widget.CheckBox;
61 import android.widget.CompoundButton;
62 import android.widget.DatePicker;
63 import android.widget.ImageButton;
64 import android.widget.LinearLayout;
65 import android.widget.ResourceCursorAdapter;
66 import android.widget.Spinner;
67 import android.widget.TextView;
68 import android.widget.TimePicker;
69 import android.widget.Toast;
71 import java.util.ArrayList;
72 import java.util.Arrays;
73 import java.util.Calendar;
74 import java.util.TimeZone;
76 public class EditEvent extends Activity implements View.OnClickListener,
77 DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
79 * This is the symbolic name for the key used to pass in the boolean
80 * for creating all-day events that is part of the extra data of the intent.
81 * This is used only for creating new events and is set to true if
82 * the default for the new event should be an all-day event.
84 public static final String EVENT_ALL_DAY = "allDay";
86 private static final int MAX_REMINDERS = 5;
88 private static final int MENU_GROUP_REMINDER = 1;
89 private static final int MENU_GROUP_SHOW_OPTIONS = 2;
90 private static final int MENU_GROUP_HIDE_OPTIONS = 3;
92 private static final int MENU_ADD_REMINDER = 1;
93 private static final int MENU_SHOW_EXTRA_OPTIONS = 2;
94 private static final int MENU_HIDE_EXTRA_OPTIONS = 3;
96 private static final String[] EVENT_PROJECTION = new String[] {
99 Events.DESCRIPTION, // 2
100 Events.EVENT_LOCATION, // 3
102 Events.HAS_ALARM, // 5
103 Events.CALENDAR_ID, // 6
105 Events.DURATION, // 8
106 Events.EVENT_TIMEZONE, // 9
108 Events._SYNC_ID, // 11
109 Events.TRANSPARENCY, // 12
110 Events.VISIBILITY, // 13
112 private static final int EVENT_INDEX_ID = 0;
113 private static final int EVENT_INDEX_TITLE = 1;
114 private static final int EVENT_INDEX_DESCRIPTION = 2;
115 private static final int EVENT_INDEX_EVENT_LOCATION = 3;
116 private static final int EVENT_INDEX_ALL_DAY = 4;
117 private static final int EVENT_INDEX_HAS_ALARM = 5;
118 private static final int EVENT_INDEX_CALENDAR_ID = 6;
119 private static final int EVENT_INDEX_DTSTART = 7;
120 private static final int EVENT_INDEX_DURATION = 8;
121 private static final int EVENT_INDEX_TIMEZONE = 9;
122 private static final int EVENT_INDEX_RRULE = 10;
123 private static final int EVENT_INDEX_SYNC_ID = 11;
124 private static final int EVENT_INDEX_TRANSPARENCY = 12;
125 private static final int EVENT_INDEX_VISIBILITY = 13;
127 private static final String[] CALENDARS_PROJECTION = new String[] {
129 Calendars.DISPLAY_NAME, // 1
130 Calendars.TIMEZONE, // 2
132 private static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
133 private static final int CALENDARS_INDEX_TIMEZONE = 2;
134 private static final String CALENDARS_WHERE = Calendars.ACCESS_LEVEL + ">=" +
135 Calendars.CONTRIBUTOR_ACCESS + " AND " + Calendars.SYNC_EVENTS + "=1";
137 private static final String[] REMINDERS_PROJECTION = new String[] {
139 Reminders.MINUTES, // 1
141 private static final int REMINDERS_INDEX_MINUTES = 1;
142 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
143 Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
144 Reminders.METHOD_DEFAULT + ")";
146 private static final int DOES_NOT_REPEAT = 0;
147 private static final int REPEATS_DAILY = 1;
148 private static final int REPEATS_EVERY_WEEKDAY = 2;
149 private static final int REPEATS_WEEKLY_ON_DAY = 3;
150 private static final int REPEATS_MONTHLY_ON_DAY_COUNT = 4;
151 private static final int REPEATS_MONTHLY_ON_DAY = 5;
152 private static final int REPEATS_YEARLY = 6;
153 private static final int REPEATS_CUSTOM = 7;
155 private static final int MODIFY_UNINITIALIZED = 0;
156 private static final int MODIFY_SELECTED = 1;
157 private static final int MODIFY_ALL = 2;
158 private static final int MODIFY_ALL_FOLLOWING = 3;
160 private static final int DAY_IN_SECONDS = 24 * 60 * 60;
162 private int mFirstDayOfWeek; // cached in onCreate
164 private Cursor mEventCursor;
165 private Cursor mCalendarsCursor;
167 private Button mStartDateButton;
168 private Button mEndDateButton;
169 private Button mStartTimeButton;
170 private Button mEndTimeButton;
171 private Button mSaveButton;
172 private Button mDeleteButton;
173 private Button mDiscardButton;
174 private CheckBox mAllDayCheckBox;
175 private Spinner mCalendarsSpinner;
176 private Spinner mRepeatsSpinner;
177 private Spinner mAvailabilitySpinner;
178 private Spinner mVisibilitySpinner;
179 private TextView mTitleTextView;
180 private TextView mLocationTextView;
181 private TextView mDescriptionTextView;
182 private View mRemindersSeparator;
183 private LinearLayout mRemindersContainer;
184 private LinearLayout mExtraOptions;
185 private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
186 private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
188 private EventRecurrence mEventRecurrence = new EventRecurrence();
189 private String mRrule;
190 private boolean mCalendarsQueryComplete;
191 private boolean mSaveAfterQueryComplete;
192 private ProgressDialog mLoadingCalendarsDialog;
193 private AlertDialog mNoCalendarsDialog;
194 private ContentValues mInitialValues;
197 * If the repeating event is created on the phone and it hasn't been
198 * synced yet to the web server, then there is a bug where you can't
199 * delete or change an instance of the repeating event. This case
200 * can be detected with mSyncId. If mSyncId == null, then the repeating
201 * event has not been synced to the phone, in which case we won't allow
202 * the user to change one instance.
204 private String mSyncId;
206 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0);
207 private ArrayList<Integer> mReminderValues;
208 private ArrayList<String> mReminderLabels;
210 private Time mStartTime;
211 private Time mEndTime;
212 private int mModification = MODIFY_UNINITIALIZED;
213 private int mDefaultReminderMinutes;
215 private DeleteEventHelper mDeleteEventHelper;
216 private QueryHandler mQueryHandler;
218 /* This class is used to update the time buttons. */
219 private class TimeListener implements OnTimeSetListener {
222 public TimeListener(View view) {
226 public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
227 // Cache the member variables locally to avoid inner class overhead.
228 Time startTime = mStartTime;
229 Time endTime = mEndTime;
231 // Cache the start and end millis so that we limit the number
232 // of calls to normalize() and toMillis(), which are fairly
236 if (mView == mStartTimeButton) {
237 // The start time was changed.
238 int hourDuration = endTime.hour - startTime.hour;
239 int minuteDuration = endTime.minute - startTime.minute;
241 startTime.hour = hourOfDay;
242 startTime.minute = minute;
243 startMillis = startTime.normalize(true);
245 // Also update the end time to keep the duration constant.
246 endTime.hour = hourOfDay + hourDuration;
247 endTime.minute = minute + minuteDuration;
248 endMillis = endTime.normalize(true);
250 // The end time was changed.
251 startMillis = startTime.toMillis(true);
252 endTime.hour = hourOfDay;
253 endTime.minute = minute;
254 endMillis = endTime.normalize(true);
256 // Do not allow an event to have an end time before the start time.
257 if (endTime.before(startTime)) {
258 endTime.set(startTime);
259 endMillis = startMillis;
263 setDate(mEndDateButton, endMillis);
264 setTime(mStartTimeButton, startMillis);
265 setTime(mEndTimeButton, endMillis);
269 private class TimeClickListener implements View.OnClickListener {
272 public TimeClickListener(Time time) {
276 public void onClick(View v) {
277 new TimePickerDialog(EditEvent.this, new TimeListener(v),
278 mTime.hour, mTime.minute,
279 DateFormat.is24HourFormat(EditEvent.this)).show();
283 private class DateListener implements OnDateSetListener {
286 public DateListener(View view) {
290 public void onDateSet(DatePicker view, int year, int month, int monthDay) {
291 // Cache the member variables locally to avoid inner class overhead.
292 Time startTime = mStartTime;
293 Time endTime = mEndTime;
295 // Cache the start and end millis so that we limit the number
296 // of calls to normalize() and toMillis(), which are fairly
300 if (mView == mStartDateButton) {
301 // The start date was changed.
302 int yearDuration = endTime.year - startTime.year;
303 int monthDuration = endTime.month - startTime.month;
304 int monthDayDuration = endTime.monthDay - startTime.monthDay;
306 startTime.year = year;
307 startTime.month = month;
308 startTime.monthDay = monthDay;
309 startMillis = startTime.normalize(true);
311 // Also update the end date to keep the duration constant.
312 endTime.year = year + yearDuration;
313 endTime.month = month + monthDuration;
314 endTime.monthDay = monthDay + monthDayDuration;
315 endMillis = endTime.normalize(true);
317 // If the start date has changed then update the repeats.
320 // The end date was changed.
321 startMillis = startTime.toMillis(true);
323 endTime.month = month;
324 endTime.monthDay = monthDay;
325 endMillis = endTime.normalize(true);
327 // Do not allow an event to have an end time before the start time.
328 if (endTime.before(startTime)) {
329 endTime.set(startTime);
330 endMillis = startMillis;
334 setDate(mStartDateButton, startMillis);
335 setDate(mEndDateButton, endMillis);
336 setTime(mEndTimeButton, endMillis); // In case end time had to be reset
340 private class DateClickListener implements View.OnClickListener {
343 public DateClickListener(Time time) {
347 public void onClick(View v) {
348 new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year,
349 mTime.month, mTime.monthDay).show();
353 static private class CalendarsAdapter extends ResourceCursorAdapter {
354 public CalendarsAdapter(Context context, Cursor c) {
355 super(context, R.layout.calendars_item, c);
356 setDropDownViewResource(R.layout.calendars_dropdown_item);
360 public void bindView(View view, Context context, Cursor cursor) {
361 TextView name = (TextView) view.findViewById(R.id.calendar_name);
362 name.setText(cursor.getString(CALENDARS_INDEX_DISPLAY_NAME));
366 // This is called if the user clicks on one of the buttons: "Save",
367 // "Discard", or "Delete". This is also called if the user clicks
368 // on the "remove reminder" button.
369 public void onClick(View v) {
370 if (v == mSaveButton) {
377 if (v == mDeleteButton) {
378 long begin = mStartTime.toMillis(false /* use isDst */);
379 long end = mEndTime.toMillis(false /* use isDst */);
381 switch (mModification) {
382 case MODIFY_SELECTED:
383 which = DeleteEventHelper.DELETE_SELECTED;
385 case MODIFY_ALL_FOLLOWING:
386 which = DeleteEventHelper.DELETE_ALL_FOLLOWING;
389 which = DeleteEventHelper.DELETE_ALL;
392 mDeleteEventHelper.delete(begin, end, mEventCursor, which);
396 if (v == mDiscardButton) {
401 // This must be a click on one of the "remove reminder" buttons
402 LinearLayout reminderItem = (LinearLayout) v.getParent();
403 LinearLayout parent = (LinearLayout) reminderItem.getParent();
404 parent.removeView(reminderItem);
405 mReminderItems.remove(reminderItem);
406 updateRemindersVisibility();
409 // This is called if the user cancels a popup dialog. There are two
410 // dialogs: the "Loading calendars" dialog, and the "No calendars"
411 // dialog. The "Loading calendars" dialog is shown if there is a delay
412 // in loading the calendars (needed when creating an event) and the user
413 // tries to save the event before the calendars have finished loading.
414 // The "No calendars" dialog is shown if there are no syncable calendars.
415 public void onCancel(DialogInterface dialog) {
416 if (dialog == mLoadingCalendarsDialog) {
417 mSaveAfterQueryComplete = false;
418 } else if (dialog == mNoCalendarsDialog) {
423 // This is called if the user clicks on a dialog button.
424 public void onClick(DialogInterface dialog, int which) {
425 if (dialog == mNoCalendarsDialog) {
430 private class QueryHandler extends AsyncQueryHandler {
431 public QueryHandler(ContentResolver cr) {
436 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
437 // If the Activity is finishing, then close the cursor.
438 // Otherwise, use the new cursor in the adapter.
440 stopManagingCursor(cursor);
443 mCalendarsCursor = cursor;
444 startManagingCursor(cursor);
447 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
448 Window.PROGRESS_VISIBILITY_OFF);
450 // If there are no syncable calendars, then we cannot allow
451 // creating a new event.
452 if (cursor.getCount() == 0) {
453 // Cancel the "loading calendars" dialog if it exists
454 if (mSaveAfterQueryComplete) {
455 mLoadingCalendarsDialog.cancel();
458 // Create an error message for the user that, when clicked,
459 // will exit this activity without saving the event.
460 AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
461 builder.setTitle(R.string.no_syncable_calendars)
462 .setIcon(android.R.drawable.ic_dialog_alert)
463 .setMessage(R.string.no_calendars_found)
464 .setPositiveButton(android.R.string.ok, EditEvent.this)
465 .setOnCancelListener(EditEvent.this);
466 mNoCalendarsDialog = builder.show();
470 // populate the calendars spinner
471 CalendarsAdapter adapter = new CalendarsAdapter(EditEvent.this, mCalendarsCursor);
472 mCalendarsSpinner.setAdapter(adapter);
473 mCalendarsQueryComplete = true;
474 if (mSaveAfterQueryComplete) {
475 mLoadingCalendarsDialog.cancel();
484 protected void onCreate(Bundle icicle) {
485 super.onCreate(icicle);
486 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
487 setContentView(R.layout.edit_event);
489 mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
491 mStartTime = new Time();
492 mEndTime = new Time();
494 Intent intent = getIntent();
495 mUri = intent.getData();
498 mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null);
499 if (mEventCursor == null || mEventCursor.getCount() == 0) {
500 // The cursor is empty. This can happen if the event was deleted.
506 long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
507 long end = intent.getLongExtra(EVENT_END_TIME, 0);
509 boolean allDay = false;
510 if (mEventCursor != null) {
511 // The event already exists so fetch the all-day status
512 mEventCursor.moveToFirst();
513 allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
514 String rrule = mEventCursor.getString(EVENT_INDEX_RRULE);
515 String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
516 long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
518 // Remember the initial values
519 mInitialValues = new ContentValues();
520 mInitialValues.put(EVENT_BEGIN_TIME, begin);
521 mInitialValues.put(EVENT_END_TIME, end);
522 mInitialValues.put(Events.ALL_DAY, allDay ? 1 : 0);
523 mInitialValues.put(Events.RRULE, rrule);
524 mInitialValues.put(Events.EVENT_TIMEZONE, timezone);
525 mInitialValues.put(Events.CALENDAR_ID, calendarId);
527 // We are creating a new event, so set the default from the
528 // intent (if specified).
529 allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false);
532 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
533 Window.PROGRESS_VISIBILITY_ON);
535 // Start a query in the background to read the list of calendars
536 mQueryHandler = new QueryHandler(getContentResolver());
537 mQueryHandler.startQuery(0, null, Calendars.CONTENT_URI, CALENDARS_PROJECTION,
538 CALENDARS_WHERE, null /* selection args */, null /* sort order */);
541 // If the event is all-day, read the times in UTC timezone
544 String tz = mStartTime.timezone;
545 mStartTime.timezone = Time.TIMEZONE_UTC;
546 mStartTime.set(begin);
547 mStartTime.timezone = tz;
549 // Calling normalize to calculate isDst
550 mStartTime.normalize(true);
552 mStartTime.set(begin);
558 String tz = mStartTime.timezone;
559 mEndTime.timezone = Time.TIMEZONE_UTC;
561 mEndTime.timezone = tz;
563 // Calling normalize to calculate isDst
564 mEndTime.normalize(true);
570 // cache all the widgets
571 mTitleTextView = (TextView) findViewById(R.id.title);
572 mLocationTextView = (TextView) findViewById(R.id.location);
573 mDescriptionTextView = (TextView) findViewById(R.id.description);
574 mStartDateButton = (Button) findViewById(R.id.start_date);
575 mEndDateButton = (Button) findViewById(R.id.end_date);
576 mStartTimeButton = (Button) findViewById(R.id.start_time);
577 mEndTimeButton = (Button) findViewById(R.id.end_time);
578 mAllDayCheckBox = (CheckBox) findViewById(R.id.is_all_day);
579 mCalendarsSpinner = (Spinner) findViewById(R.id.calendars);
580 mRepeatsSpinner = (Spinner) findViewById(R.id.repeats);
581 mAvailabilitySpinner = (Spinner) findViewById(R.id.availability);
582 mVisibilitySpinner = (Spinner) findViewById(R.id.visibility);
583 mRemindersSeparator = findViewById(R.id.reminders_separator);
584 mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
585 mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
587 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
588 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
590 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
592 long endMillis = mEndTime.normalize(true);
594 // Do not allow an event to have an end time before the start time.
595 if (mEndTime.before(mStartTime)) {
596 mEndTime.set(mStartTime);
597 endMillis = mEndTime.normalize(true);
599 setDate(mEndDateButton, endMillis);
600 setTime(mEndTimeButton, endMillis);
603 mStartTimeButton.setVisibility(View.GONE);
604 mEndTimeButton.setVisibility(View.GONE);
606 if (mEndTime.hour == 0 && mEndTime.minute == 0) {
608 long endMillis = mEndTime.normalize(true);
609 setDate(mEndDateButton, endMillis);
610 setTime(mEndTimeButton, endMillis);
613 mStartTimeButton.setVisibility(View.VISIBLE);
614 mEndTimeButton.setVisibility(View.VISIBLE);
620 mAllDayCheckBox.setChecked(true);
622 mAllDayCheckBox.setChecked(false);
625 mSaveButton = (Button) findViewById(R.id.save);
626 mSaveButton.setOnClickListener(this);
628 mDeleteButton = (Button) findViewById(R.id.delete);
629 mDeleteButton.setOnClickListener(this);
631 mDiscardButton = (Button) findViewById(R.id.discard);
632 mDiscardButton.setOnClickListener(this);
634 // Initialize the reminder values array.
635 Resources r = getResources();
636 String[] strings = r.getStringArray(R.array.reminder_minutes_values);
637 int size = strings.length;
638 ArrayList<Integer> list = new ArrayList<Integer>(size);
639 for (int i = 0 ; i < size ; i++) {
640 list.add(Integer.parseInt(strings[i]));
642 mReminderValues = list;
643 String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
644 mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
646 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
647 String durationString =
648 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
649 mDefaultReminderMinutes = Integer.parseInt(durationString);
652 boolean hasAlarm = (mEventCursor != null)
653 && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0);
655 Uri uri = Reminders.CONTENT_URI;
656 long eventId = mEventCursor.getLong(EVENT_INDEX_ID);
657 String where = String.format(REMINDERS_WHERE, eventId);
658 ContentResolver cr = getContentResolver();
659 Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
661 // First pass: collect all the custom reminder minutes (e.g.,
662 // a reminder of 8 minutes) into a global list.
663 while (reminderCursor.moveToNext()) {
664 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
665 EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
668 // Second pass: create the reminder spinners
669 reminderCursor.moveToPosition(-1);
670 while (reminderCursor.moveToNext()) {
671 int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
672 mOriginalMinutes.add(minutes);
673 EditEvent.addReminder(this, this, mReminderItems, mReminderValues,
674 mReminderLabels, minutes);
677 reminderCursor.close();
680 updateRemindersVisibility();
682 // Setup the + Add Reminder Button
683 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
684 public void onClick(View v) {
688 ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
689 reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
691 mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
693 if (mEventCursor == null) {
694 // Allow the intent to specify the fields in the event.
695 // This will allow other apps to create events easily.
696 initFromIntent(intent);
700 private void initFromIntent(Intent intent) {
701 String title = intent.getStringExtra(Events.TITLE);
703 mTitleTextView.setText(title);
706 String location = intent.getStringExtra(Events.EVENT_LOCATION);
707 if (location != null) {
708 mLocationTextView.setText(location);
711 String description = intent.getStringExtra(Events.DESCRIPTION);
712 if (description != null) {
713 mDescriptionTextView.setText(description);
716 int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
717 if (availability != -1) {
718 mAvailabilitySpinner.setSelection(availability);
721 int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
722 if (visibility != -1) {
723 mVisibilitySpinner.setSelection(visibility);
726 String rrule = intent.getStringExtra(Events.RRULE);
729 mEventRecurrence.parse(rrule);
734 protected void onResume() {
738 if (mEventCursor == null || mEventCursor.getCount() == 0) {
739 // The cursor is empty. This can happen if the event was deleted.
745 if (mEventCursor != null) {
746 Cursor cursor = mEventCursor;
747 cursor.moveToFirst();
749 mRrule = cursor.getString(EVENT_INDEX_RRULE);
750 String title = cursor.getString(EVENT_INDEX_TITLE);
751 String description = cursor.getString(EVENT_INDEX_DESCRIPTION);
752 String location = cursor.getString(EVENT_INDEX_EVENT_LOCATION);
753 int availability = cursor.getInt(EVENT_INDEX_TRANSPARENCY);
754 int visibility = cursor.getInt(EVENT_INDEX_VISIBILITY);
755 if (visibility > 0) {
756 // For now we the array contains the values 0, 2, and 3. We subtract one to match.
760 if (!TextUtils.isEmpty(mRrule) && mModification == MODIFY_UNINITIALIZED) {
761 // If this event has not been synced, then don't allow deleting
762 // or changing a single instance.
763 mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
764 mEventRecurrence.parse(mRrule);
766 // If we haven't synced this repeating event yet, then don't
767 // allow the user to change just one instance.
769 CharSequence[] items;
770 if (mSyncId == null) {
771 items = new CharSequence[2];
773 items = new CharSequence[3];
774 items[itemIndex++] = getText(R.string.modify_event);
776 items[itemIndex++] = getText(R.string.modify_all);
777 items[itemIndex++] = getText(R.string.modify_all_following);
779 // Display the modification dialog.
780 new AlertDialog.Builder(this)
781 .setOnCancelListener(new OnCancelListener() {
782 public void onCancel(DialogInterface dialog) {
786 .setTitle(R.string.edit_event_label)
787 .setItems(items, new OnClickListener() {
788 public void onClick(DialogInterface dialog, int which) {
791 (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED;
792 } else if (which == 1) {
794 (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL;
795 } else if (which == 2) {
796 mModification = MODIFY_ALL_FOLLOWING;
799 // If we are modifying all the events in a
800 // series then disable and ignore the date.
801 if (mModification == MODIFY_ALL) {
802 mStartDateButton.setEnabled(false);
803 mEndDateButton.setEnabled(false);
804 } else if (mModification == MODIFY_SELECTED) {
805 mRepeatsSpinner.setEnabled(false);
812 mTitleTextView.setText(title);
813 mLocationTextView.setText(location);
814 mDescriptionTextView.setText(description);
815 mAvailabilitySpinner.setSelection(availability);
816 mVisibilitySpinner.setSelection(visibility);
818 // This is an existing event so hide the calendar spinner
819 // since we can't change the calendar.
820 View calendarGroup = findViewById(R.id.calendar_group);
821 calendarGroup.setVisibility(View.GONE);
822 } else if (Time.isEpoch(mStartTime) && Time.isEpoch(mEndTime)) {
823 mStartTime.setToNow();
825 // Round the time to the nearest half hour.
826 mStartTime.second = 0;
827 int minute = mStartTime.minute;
828 if (minute > 0 && minute <= 30) {
829 mStartTime.minute = 30;
831 mStartTime.minute = 0;
832 mStartTime.hour += 1;
835 long startMillis = mStartTime.normalize(true /* ignore isDst */);
836 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS);
838 // New event - set the default reminder
839 if (mDefaultReminderMinutes != 0) {
840 addReminder(this, this, mReminderItems, mReminderValues,
841 mReminderLabels, mDefaultReminderMinutes);
844 // Hide delete button
845 mDeleteButton.setVisibility(View.GONE);
848 updateRemindersVisibility();
854 public boolean onCreateOptionsMenu(Menu menu) {
856 item = menu.add(MENU_GROUP_REMINDER, MENU_ADD_REMINDER, 0,
857 R.string.add_new_reminder);
858 item.setIcon(R.drawable.ic_menu_reminder);
859 item.setAlphabeticShortcut('r');
861 item = menu.add(MENU_GROUP_SHOW_OPTIONS, MENU_SHOW_EXTRA_OPTIONS, 0,
862 R.string.edit_event_show_extra_options);
863 item.setIcon(R.drawable.ic_menu_show_list);
864 item = menu.add(MENU_GROUP_HIDE_OPTIONS, MENU_HIDE_EXTRA_OPTIONS, 0,
865 R.string.edit_event_hide_extra_options);
866 item.setIcon(R.drawable.ic_menu_show_list);
868 return super.onCreateOptionsMenu(menu);
872 public boolean onPrepareOptionsMenu(Menu menu) {
873 if (mReminderItems.size() < MAX_REMINDERS) {
874 menu.setGroupVisible(MENU_GROUP_REMINDER, true);
875 menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
877 menu.setGroupVisible(MENU_GROUP_REMINDER, false);
878 menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
881 if (mExtraOptions.getVisibility() == View.VISIBLE) {
882 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false);
883 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true);
885 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true);
886 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false);
889 return super.onPrepareOptionsMenu(menu);
892 private void addReminder() {
893 // TODO: when adding a new reminder, make it different from the
894 // last one in the list (if any).
895 if (mDefaultReminderMinutes == 0) {
896 addReminder(this, this, mReminderItems, mReminderValues,
897 mReminderLabels, 10 /* minutes */);
899 addReminder(this, this, mReminderItems, mReminderValues,
900 mReminderLabels, mDefaultReminderMinutes);
902 updateRemindersVisibility();
906 public boolean onOptionsItemSelected(MenuItem item) {
907 switch (item.getItemId()) {
908 case MENU_ADD_REMINDER:
911 case MENU_SHOW_EXTRA_OPTIONS:
912 mExtraOptions.setVisibility(View.VISIBLE);
914 case MENU_HIDE_EXTRA_OPTIONS:
915 mExtraOptions.setVisibility(View.GONE);
918 return super.onOptionsItemSelected(item);
922 public boolean onKeyDown(int keyCode, KeyEvent event) {
924 case KeyEvent.KEYCODE_BACK:
925 // If we are creating a new event, do not create it if the
926 // title, location and description are all empty, in order to
927 // prevent accidental "no subject" event creations.
928 if (mUri != null || !isEmpty()) {
930 // We cannot exit this activity because the calendars
931 // are still loading.
938 return super.onKeyDown(keyCode, event);
941 private void populateWhen() {
942 long startMillis = mStartTime.toMillis(false /* use isDst */);
943 long endMillis = mEndTime.toMillis(false /* use isDst */);
944 setDate(mStartDateButton, startMillis);
945 setDate(mEndDateButton, endMillis);
947 setTime(mStartTimeButton, startMillis);
948 setTime(mEndTimeButton, endMillis);
950 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime));
951 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime));
953 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime));
954 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime));
957 private void populateRepeats() {
958 Time time = mStartTime;
959 Resources r = getResources();
960 int resource = android.R.layout.simple_spinner_item;
962 String[] days = new String[] {
963 DateUtils.getDayOfWeekString(Calendar.SUNDAY, DateUtils.LENGTH_MEDIUM),
964 DateUtils.getDayOfWeekString(Calendar.MONDAY, DateUtils.LENGTH_MEDIUM),
965 DateUtils.getDayOfWeekString(Calendar.TUESDAY, DateUtils.LENGTH_MEDIUM),
966 DateUtils.getDayOfWeekString(Calendar.WEDNESDAY, DateUtils.LENGTH_MEDIUM),
967 DateUtils.getDayOfWeekString(Calendar.THURSDAY, DateUtils.LENGTH_MEDIUM),
968 DateUtils.getDayOfWeekString(Calendar.FRIDAY, DateUtils.LENGTH_MEDIUM),
969 DateUtils.getDayOfWeekString(Calendar.SATURDAY, DateUtils.LENGTH_MEDIUM),
971 String[] ordinals = r.getStringArray(R.array.ordinal_labels);
973 // Only display "Custom" in the spinner if the device does not support the
974 // recurrence functionality of the event. Only display every weekday if
975 // the event starts on a weekday.
976 boolean isCustomRecurrence = isCustomRecurrence();
977 boolean isWeekdayEvent = isWeekdayEvent();
979 ArrayList<String> repeatArray = new ArrayList<String>(0);
980 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0);
982 repeatArray.add(r.getString(R.string.does_not_repeat));
983 recurrenceIndexes.add(DOES_NOT_REPEAT);
985 repeatArray.add(r.getString(R.string.daily));
986 recurrenceIndexes.add(REPEATS_DAILY);
988 if (isWeekdayEvent) {
989 repeatArray.add(r.getString(R.string.every_weekday));
990 recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY);
993 String format = r.getString(R.string.weekly);
994 repeatArray.add(String.format(format, time.format("%A")));
995 recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY);
997 // Calculate whether this is the 1st, 2nd, 3rd, 4th, or last appearance of the given day.
998 int dayNumber = (time.monthDay - 1) / 7;
999 format = r.getString(R.string.monthly_on_day_count);
1000 repeatArray.add(String.format(format, ordinals[dayNumber], days[time.weekDay]));
1001 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY_COUNT);
1003 format = r.getString(R.string.monthly_on_day);
1004 repeatArray.add(String.format(format, time.monthDay));
1005 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY);
1007 long when = time.toMillis(false);
1008 format = r.getString(R.string.yearly);
1010 if (DateFormat.is24HourFormat(this)) {
1011 flags |= DateUtils.FORMAT_24HOUR;
1013 repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags)));
1014 recurrenceIndexes.add(REPEATS_YEARLY);
1016 if (isCustomRecurrence) {
1017 repeatArray.add(r.getString(R.string.custom));
1018 recurrenceIndexes.add(REPEATS_CUSTOM);
1020 mRecurrenceIndexes = recurrenceIndexes;
1022 int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT);
1023 if (mRrule != null) {
1024 if (isCustomRecurrence) {
1025 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM);
1027 switch (mEventRecurrence.freq) {
1028 case EventRecurrence.DAILY:
1029 position = recurrenceIndexes.indexOf(REPEATS_DAILY);
1031 case EventRecurrence.WEEKLY:
1032 if (mEventRecurrence.repeatsOnEveryWeekDay()) {
1033 position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY);
1035 position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY);
1038 case EventRecurrence.MONTHLY:
1039 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1040 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT);
1042 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY);
1045 case EventRecurrence.YEARLY:
1046 position = recurrenceIndexes.indexOf(REPEATS_YEARLY);
1051 ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, resource, repeatArray);
1052 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1053 mRepeatsSpinner.setAdapter(adapter);
1054 mRepeatsSpinner.setSelection(position);
1057 // Adds a reminder to the displayed list of reminders.
1058 // Returns true if successfully added reminder, false if no reminders can
1060 static boolean addReminder(Activity activity, View.OnClickListener listener,
1061 ArrayList<LinearLayout> items, ArrayList<Integer> values,
1062 ArrayList<String> labels, int minutes) {
1064 if (items.size() >= MAX_REMINDERS) {
1068 LayoutInflater inflater = activity.getLayoutInflater();
1069 LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
1070 LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
1071 parent.addView(reminderItem);
1073 Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
1074 Resources res = activity.getResources();
1075 spinner.setPrompt(res.getString(R.string.reminders_label));
1076 int resource = android.R.layout.simple_spinner_item;
1077 ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
1078 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1079 spinner.setAdapter(adapter);
1081 ImageButton reminderRemoveButton;
1082 reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
1083 reminderRemoveButton.setOnClickListener(listener);
1085 int index = findMinutesInReminderList(values, minutes);
1086 spinner.setSelection(index);
1087 items.add(reminderItem);
1092 static void addMinutesToList(Context context, ArrayList<Integer> values,
1093 ArrayList<String> labels, int minutes) {
1094 int index = values.indexOf(minutes);
1099 // The requested "minutes" does not exist in the list, so insert it
1102 String label = constructReminderLabel(context, minutes, false);
1103 int len = values.size();
1104 for (int i = 0; i < len; i++) {
1105 if (minutes < values.get(i)) {
1106 values.add(i, minutes);
1107 labels.add(i, label);
1112 values.add(minutes);
1113 labels.add(len, label);
1117 * Finds the index of the given "minutes" in the "values" list.
1119 * @param values the list of minutes corresponding to the spinner choices
1120 * @param minutes the minutes to search for in the values list
1121 * @return the index of "minutes" in the "values" list
1123 private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) {
1124 int index = values.indexOf(minutes);
1126 // This should never happen.
1127 Log.e("Cal", "Cannot find minutes (" + minutes + ") in list");
1133 // Constructs a label given an arbitrary number of minutes. For example,
1134 // if the given minutes is 63, then this returns the string "63 minutes".
1135 // As another example, if the given minutes is 120, then this returns
1137 static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
1138 Resources resources = context.getResources();
1141 if (minutes % 60 != 0) {
1144 resId = R.plurals.Nmins;
1146 resId = R.plurals.Nminutes;
1148 } else if (minutes % (24 * 60) != 0) {
1149 value = minutes / 60;
1150 resId = R.plurals.Nhours;
1152 value = minutes / ( 24 * 60);
1153 resId = R.plurals.Ndays;
1156 String format = resources.getQuantityString(resId, value);
1157 return String.format(format, value);
1160 private void updateRemindersVisibility() {
1161 if (mReminderItems.size() == 0) {
1162 mRemindersSeparator.setVisibility(View.GONE);
1163 mRemindersContainer.setVisibility(View.GONE);
1165 mRemindersSeparator.setVisibility(View.VISIBLE);
1166 mRemindersContainer.setVisibility(View.VISIBLE);
1170 private void setDate(TextView view, long millis) {
1171 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
1172 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_MONTH |
1173 DateUtils.FORMAT_ABBREV_WEEKDAY;
1174 view.setText(DateUtils.formatDateTime(this, millis, flags));
1177 private void setTime(TextView view, long millis) {
1178 int flags = DateUtils.FORMAT_SHOW_TIME;
1179 if (DateFormat.is24HourFormat(this)) {
1180 flags |= DateUtils.FORMAT_24HOUR;
1182 view.setText(DateUtils.formatDateTime(this, millis, flags));
1185 // Saves the event. Returns true if it is okay to exit this activity.
1186 private boolean save() {
1187 boolean forceSaveReminders = false;
1189 // If we are creating a new event, then make sure we wait until the
1190 // query to fetch the list of calendars has finished.
1191 if (mEventCursor == null) {
1192 if (!mCalendarsQueryComplete) {
1193 // Wait for the calendars query to finish.
1194 if (mLoadingCalendarsDialog == null) {
1195 // Create the progress dialog
1196 mLoadingCalendarsDialog = ProgressDialog.show(this,
1197 getText(R.string.loading_calendars_title),
1198 getText(R.string.loading_calendars_message),
1200 mSaveAfterQueryComplete = true;
1205 // Avoid creating a new event if the calendars cursor is empty. This
1206 // shouldn't ever happen since the setup wizard should ensure the user
1208 if (mCalendarsCursor == null || mCalendarsCursor.getCount() == 0) {
1209 Log.w("Cal", "The calendars table does not contain any calendars."
1210 + " New event was not created.");
1213 Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show();
1215 Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
1218 ContentResolver cr = getContentResolver();
1219 ContentValues values = getContentValuesFromUi();
1222 // For recurring events, we must make sure that we use duration rather
1225 // Create new event with new contents
1226 addRecurrenceRule(values);
1227 uri = cr.insert(Events.CONTENT_URI, values);
1228 forceSaveReminders = true;
1230 } else if (mRrule == null) {
1231 // Modify contents of a non-repeating event
1232 addRecurrenceRule(values);
1233 checkTimeDependentFields(values);
1234 cr.update(uri, values, null, null);
1236 } else if (mInitialValues.getAsString(Events.RRULE) == null) {
1237 // This event was changed from a non-repeating event to a
1239 addRecurrenceRule(values);
1240 values.remove(Events.DTEND);
1241 cr.update(uri, values, null, null);
1243 } else if (mModification == MODIFY_SELECTED) {
1244 // Modify contents of the current instance of repeating event
1246 // Create a recurrence exception
1247 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1248 values.put(Events.ORIGINAL_EVENT, mEventCursor.getString(EVENT_INDEX_SYNC_ID));
1249 values.put(Events.ORIGINAL_INSTANCE_TIME, begin);
1250 boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1251 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
1253 uri = cr.insert(Events.CONTENT_URI, values);
1254 forceSaveReminders = true;
1256 } else if (mModification == MODIFY_ALL_FOLLOWING) {
1257 // Modify this instance and all future instances of repeating event
1258 addRecurrenceRule(values);
1260 if (mRrule == null) {
1261 // We've changed a recurring event to a non-recurring event.
1262 // If the event we are editing is the first in the series,
1263 // then delete the whole series. Otherwise, update the series
1264 // to end at the new start time.
1265 if (isFirstEventInSeries()) {
1266 cr.delete(uri, null, null);
1268 // Update the current repeating event to end at the new
1270 updatePastEvents(cr, uri);
1272 uri = cr.insert(Events.CONTENT_URI, values);
1274 if (isFirstEventInSeries()) {
1275 checkTimeDependentFields(values);
1276 values.remove(Events.DTEND);
1277 cr.update(uri, values, null, null);
1279 // Update the current repeating event to end at the new
1281 updatePastEvents(cr, uri);
1283 // Create a new event with the user-modified fields
1284 values.remove(Events.DTEND);
1285 uri = cr.insert(Events.CONTENT_URI, values);
1288 forceSaveReminders = true;
1290 } else if (mModification == MODIFY_ALL) {
1292 // Modify all instances of repeating event
1293 addRecurrenceRule(values);
1295 if (mRrule == null) {
1296 // We've changed a recurring event to a non-recurring event.
1297 // Delete the whole series and replace it with a new
1298 // non-recurring event.
1299 cr.delete(uri, null, null);
1300 uri = cr.insert(Events.CONTENT_URI, values);
1301 forceSaveReminders = true;
1303 checkTimeDependentFields(values);
1304 values.remove(Events.DTEND);
1305 cr.update(uri, values, null, null);
1310 long eventId = ContentUris.parseId(uri);
1311 ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
1313 saveReminders(cr, eventId, reminderMinutes, mOriginalMinutes,
1314 forceSaveReminders);
1319 private boolean isFirstEventInSeries() {
1320 int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART);
1321 long start = mEventCursor.getLong(dtStart);
1322 return start == mStartTime.toMillis(true);
1325 private void updatePastEvents(ContentResolver cr, Uri uri) {
1326 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1327 String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
1328 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
1329 String oldRrule = mEventCursor.getString(EVENT_INDEX_RRULE);
1330 mEventRecurrence.parse(oldRrule);
1332 Time untilTime = new Time();
1333 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1334 ContentValues oldValues = new ContentValues();
1336 // The "until" time must be in UTC time in order for Google calendar
1337 // to display it properly. For all-day events, the "until" time string
1338 // must include just the date field, and not the time field. The
1339 // repeating events repeat up to and including the "until" time.
1340 untilTime.timezone = Time.TIMEZONE_UTC;
1342 // Subtract one second from the old begin time to get the new
1344 untilTime.set(begin - 1000); // subtract one second (1000 millis)
1347 untilTime.minute = 0;
1348 untilTime.second = 0;
1349 untilTime.allDay = true;
1350 untilTime.normalize(false);
1352 // For all-day events, the duration must be in days, not seconds.
1353 // Otherwise, Google Calendar will (mistakenly) change this event
1354 // into a non-all-day event.
1355 int len = oldDuration.length();
1356 if (oldDuration.charAt(0) == 'P' && oldDuration.charAt(len - 1) == 'S') {
1357 int seconds = Integer.parseInt(oldDuration.substring(1, len - 1));
1358 int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1359 oldDuration = "P" + days + "D";
1362 mEventRecurrence.until = untilTime.format2445();
1364 oldValues.put(Events.DTSTART, oldStartMillis);
1365 oldValues.put(Events.DURATION, oldDuration);
1366 oldValues.put(Events.RRULE, mEventRecurrence.toString());
1367 cr.update(uri, oldValues, null, null);
1370 private void checkTimeDependentFields(ContentValues values) {
1371 long oldBegin = mInitialValues.getAsLong(EVENT_BEGIN_TIME);
1372 long oldEnd = mInitialValues.getAsLong(EVENT_END_TIME);
1373 boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
1374 String oldRrule = mInitialValues.getAsString(Events.RRULE);
1375 String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
1377 long newBegin = values.getAsLong(Events.DTSTART);
1378 long newEnd = values.getAsLong(Events.DTEND);
1379 boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
1380 String newRrule = values.getAsString(Events.RRULE);
1381 String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1383 // If none of the time-dependent fields changed, then remove them.
1384 if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
1385 && TextUtils.equals(oldRrule, newRrule)
1386 && TextUtils.equals(oldTimezone, newTimezone)) {
1387 values.remove(Events.DTSTART);
1388 values.remove(Events.DTEND);
1389 values.remove(Events.DURATION);
1390 values.remove(Events.ALL_DAY);
1391 values.remove(Events.RRULE);
1392 values.remove(Events.EVENT_TIMEZONE);
1396 if (oldRrule == null || newRrule == null) {
1400 // If we are modifying all events then we need to set DTSTART to the
1401 // start time of the first event in the series, not the current
1402 // date and time. If the start time of the event was changed
1403 // (from, say, 3pm to 4pm), then we want to add the time difference
1404 // to the start time of the first event in the series (the DTSTART
1405 // value). If we are modifying one instance or all following instances,
1406 // then we leave the DTSTART field alone.
1407 if (mModification == MODIFY_ALL) {
1408 long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
1409 if (oldBegin != newBegin) {
1410 // The user changed the start time of this event
1411 long offset = newBegin - oldBegin;
1412 oldStartMillis += offset;
1414 values.put(Events.DTSTART, oldStartMillis);
1418 static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
1419 ArrayList<Integer> reminderValues) {
1420 int len = reminderItems.size();
1421 ArrayList<Integer> reminderMinutes = new ArrayList<Integer>(len);
1422 for (int index = 0; index < len; index++) {
1423 LinearLayout layout = reminderItems.get(index);
1424 Spinner spinner = (Spinner) layout.findViewById(R.id.reminder_value);
1425 int minutes = reminderValues.get(spinner.getSelectedItemPosition());
1426 reminderMinutes.add(minutes);
1428 return reminderMinutes;
1432 * Saves the reminders, if they changed. Returns true if the database
1435 * @param cr the ContentResolver
1436 * @param eventId the id of the event whose reminders are being updated
1437 * @param reminderMinutes the array of reminders set by the user
1438 * @param originalMinutes the original array of reminders
1439 * @param forceSave if true, then save the reminders even if they didn't
1441 * @return true if the database was updated
1443 static boolean saveReminders(ContentResolver cr, long eventId,
1444 ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
1445 boolean forceSave) {
1446 // If the reminders have not changed, then don't update the database
1447 if (reminderMinutes.equals(originalMinutes) && !forceSave) {
1451 // Delete all the existing reminders for this event
1452 String where = Reminders.EVENT_ID + "=?";
1453 String[] args = new String[] { Long.toString(eventId) };
1454 cr.delete(Reminders.CONTENT_URI, where, args);
1456 // Update the "hasAlarm" field for the event
1457 ContentValues values = new ContentValues();
1458 int len = reminderMinutes.size();
1459 values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
1460 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
1461 cr.update(uri, values, null /* where */, null /* selection args */);
1463 // Insert the new reminders, if any
1464 for (int i = 0; i < len; i++) {
1465 int minutes = reminderMinutes.get(i);
1468 values.put(Reminders.MINUTES, minutes);
1469 values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
1470 values.put(Reminders.EVENT_ID, eventId);
1471 cr.insert(Reminders.CONTENT_URI, values);
1476 private void addRecurrenceRule(ContentValues values) {
1477 updateRecurrenceRule();
1479 if (mRrule == null) {
1483 values.put(Events.RRULE, mRrule);
1484 long end = mEndTime.toMillis(true /* ignore dst */);
1485 long start = mStartTime.toMillis(true /* ignore dst */);
1488 boolean isAllDay = mAllDayCheckBox.isChecked();
1490 long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS;
1491 duration = "P" + days + "D";
1493 long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS;
1494 duration = "P" + seconds + "S";
1496 values.put(Events.DURATION, duration);
1499 private void updateRecurrenceRule() {
1500 int position = mRepeatsSpinner.getSelectedItemPosition();
1501 int selection = mRecurrenceIndexes.get(position);
1503 if (selection == DOES_NOT_REPEAT) {
1506 } else if (selection == REPEATS_CUSTOM) {
1507 // Keep custom recurrence as before.
1509 } else if (selection == REPEATS_DAILY) {
1510 mEventRecurrence.freq = EventRecurrence.DAILY;
1511 } else if (selection == REPEATS_EVERY_WEEKDAY) {
1512 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1514 int[] byday = new int[dayCount];
1515 int[] bydayNum = new int[dayCount];
1517 byday[0] = EventRecurrence.MO;
1518 byday[1] = EventRecurrence.TU;
1519 byday[2] = EventRecurrence.WE;
1520 byday[3] = EventRecurrence.TH;
1521 byday[4] = EventRecurrence.FR;
1522 for (int day = 0; day < dayCount; day++) {
1526 mEventRecurrence.byday = byday;
1527 mEventRecurrence.bydayNum = bydayNum;
1528 mEventRecurrence.bydayCount = dayCount;
1529 } else if (selection == REPEATS_WEEKLY_ON_DAY) {
1530 mEventRecurrence.freq = EventRecurrence.WEEKLY;
1531 int[] days = new int[1];
1533 int[] dayNum = new int[dayCount];
1535 days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1536 // not sure why this needs to be zero, but set it for now.
1539 mEventRecurrence.byday = days;
1540 mEventRecurrence.bydayNum = dayNum;
1541 mEventRecurrence.bydayCount = dayCount;
1542 } else if (selection == REPEATS_MONTHLY_ON_DAY) {
1543 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1544 mEventRecurrence.bydayCount = 0;
1545 mEventRecurrence.bymonthdayCount = 1;
1546 int[] bymonthday = new int[1];
1547 bymonthday[0] = mStartTime.monthDay;
1548 mEventRecurrence.bymonthday = bymonthday;
1549 } else if (selection == REPEATS_MONTHLY_ON_DAY_COUNT) {
1550 mEventRecurrence.freq = EventRecurrence.MONTHLY;
1551 mEventRecurrence.bydayCount = 1;
1552 mEventRecurrence.bymonthdayCount = 0;
1554 int[] byday = new int[1];
1555 int[] bydayNum = new int[1];
1556 // Compute the week number (for example, the "2nd" Monday)
1557 int dayCount = 1 + ((mStartTime.monthDay - 1) / 7);
1558 if (dayCount == 5) {
1561 bydayNum[0] = dayCount;
1562 byday[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay);
1563 mEventRecurrence.byday = byday;
1564 mEventRecurrence.bydayNum = bydayNum;
1565 } else if (selection == REPEATS_YEARLY) {
1566 mEventRecurrence.freq = EventRecurrence.YEARLY;
1569 // Set the week start day.
1570 mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek);
1571 mRrule = mEventRecurrence.toString();
1574 private ContentValues getContentValuesFromUi() {
1575 String title = mTitleTextView.getText().toString();
1576 boolean isAllDay = mAllDayCheckBox.isChecked();
1577 String location = mLocationTextView.getText().toString();
1578 String description = mDescriptionTextView.getText().toString();
1580 ContentValues values = new ContentValues();
1582 String timezone = null;
1587 // Reset start and end time, increment the monthDay by 1, and set
1588 // the timezone to UTC, as required for all-day events.
1589 timezone = Time.TIMEZONE_UTC;
1590 mStartTime.hour = 0;
1591 mStartTime.minute = 0;
1592 mStartTime.second = 0;
1593 mStartTime.timezone = timezone;
1594 startMillis = mStartTime.normalize(true);
1597 mEndTime.minute = 0;
1598 mEndTime.second = 0;
1599 mEndTime.monthDay++;
1600 mEndTime.timezone = timezone;
1601 endMillis = mEndTime.normalize(true);
1603 if (mEventCursor == null) {
1604 // This is a new event
1605 calendarId = mCalendarsSpinner.getSelectedItemId();
1607 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1610 startMillis = mStartTime.toMillis(true);
1611 endMillis = mEndTime.toMillis(true);
1612 if (mEventCursor != null) {
1613 // This is an existing event
1614 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
1616 // The timezone might be null if we are changing an existing
1617 // all-day event to a non-all-day event. We need to assign
1618 // a timezone to the non-all-day event.
1619 if (TextUtils.isEmpty(timezone)) {
1620 timezone = TimeZone.getDefault().getID();
1622 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID);
1624 // This is a new event
1625 calendarId = mCalendarsSpinner.getSelectedItemId();
1627 // The timezone for a new event is the currently displayed
1628 // timezone, NOT the timezone of the containing calendar.
1629 timezone = TimeZone.getDefault().getID();
1633 values.put(Events.CALENDAR_ID, calendarId);
1634 values.put(Events.EVENT_TIMEZONE, timezone);
1635 values.put(Events.TITLE, title);
1636 values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
1637 values.put(Events.DTSTART, startMillis);
1638 values.put(Events.DTEND, endMillis);
1639 values.put(Events.DESCRIPTION, description);
1640 values.put(Events.EVENT_LOCATION, location);
1641 values.put(Events.TRANSPARENCY, mAvailabilitySpinner.getSelectedItemPosition());
1643 int visibility = mVisibilitySpinner.getSelectedItemPosition();
1644 if (visibility > 0) {
1645 // For now we the array contains the values 0, 2, and 3. We add one to match.
1648 values.put(Events.VISIBILITY, visibility);
1653 private boolean isEmpty() {
1654 String title = mTitleTextView.getText().toString();
1655 if (title.length() > 0) {
1659 String location = mLocationTextView.getText().toString();
1660 if (location.length() > 0) {
1664 String description = mDescriptionTextView.getText().toString();
1665 if (description.length() > 0) {
1672 private boolean isCustomRecurrence() {
1674 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) {
1678 if (mEventRecurrence.freq == 0) {
1682 switch (mEventRecurrence.freq) {
1683 case EventRecurrence.DAILY:
1685 case EventRecurrence.WEEKLY:
1686 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) {
1688 } else if (mEventRecurrence.bydayCount == 1) {
1692 case EventRecurrence.MONTHLY:
1693 if (mEventRecurrence.repeatsMonthlyOnDayCount()) {
1695 } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) {
1699 case EventRecurrence.YEARLY:
1706 private boolean isWeekdayEvent() {
1707 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) {