1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17 package com.android.calendar; 18 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; 70 71 import java.util.ArrayList; 72 import java.util.Arrays; 73 import java.util.Calendar; 74 import java.util.TimeZone; 75 76 public class EditEvent extends Activity implements View.OnClickListener, 77 DialogInterface.OnCancelListener, DialogInterface.OnClickListener { 78 /** 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. 83 */ 84 public static final String EVENT_ALL_DAY = "allDay"; 85 86 private static final int MAX_REMINDERS = 5; 87 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; 91 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; 95 96 private static final String[] EVENT_PROJECTION = new String[] { 97 Events._ID, // 0 98 Events.TITLE, // 1 99 Events.DESCRIPTION, // 2 100 Events.EVENT_LOCATION, // 3 101 Events.ALL_DAY, // 4 102 Events.HAS_ALARM, // 5 103 Events.CALENDAR_ID, // 6 104 Events.DTSTART, // 7 105 Events.DURATION, // 8 106 Events.EVENT_TIMEZONE, // 9 107 Events.RRULE, // 10 108 Events._SYNC_ID, // 11 109 Events.TRANSPARENCY, // 12 110 Events.VISIBILITY, // 13 111 }; 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; 126 127 private static final String[] CALENDARS_PROJECTION = new String[] { 128 Calendars._ID, // 0 129 Calendars.DISPLAY_NAME, // 1 130 Calendars.TIMEZONE, // 2 131 }; 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"; 136 137 private static final String[] REMINDERS_PROJECTION = new String[] { 138 Reminders._ID, // 0 139 Reminders.MINUTES, // 1 140 }; 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 + ")"; 145 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; 154 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; 159 160 private static final int DAY_IN_SECONDS = 24 * 60 * 60; 161 162 private int mFirstDayOfWeek; // cached in onCreate 163 private Uri mUri; 164 private Cursor mEventCursor; 165 private Cursor mCalendarsCursor; 166 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); 187 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; 195 196 /** 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. 203 */ 204 private String mSyncId; 205 206 private ArrayList<Integer> mRecurrenceIndexes = new ArrayList<Integer> (0); 207 private ArrayList<Integer> mReminderValues; 208 private ArrayList<String> mReminderLabels; 209 210 private Time mStartTime; 211 private Time mEndTime; 212 private int mModification = MODIFY_UNINITIALIZED; 213 private int mDefaultReminderMinutes; 214 215 private DeleteEventHelper mDeleteEventHelper; 216 private QueryHandler mQueryHandler; 217 218 /* This class is used to update the time buttons. */ 219 private class TimeListener implements OnTimeSetListener { 220 private View mView; 221 TimeListener(View view)222 public TimeListener(View view) { 223 mView = view; 224 } 225 onTimeSet(TimePicker view, int hourOfDay, int minute)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; 230 231 // Cache the start and end millis so that we limit the number 232 // of calls to normalize() and toMillis(), which are fairly 233 // expensive. 234 long startMillis; 235 long endMillis; 236 if (mView == mStartTimeButton) { 237 // The start time was changed. 238 int hourDuration = endTime.hour - startTime.hour; 239 int minuteDuration = endTime.minute - startTime.minute; 240 241 startTime.hour = hourOfDay; 242 startTime.minute = minute; 243 startMillis = startTime.normalize(true); 244 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); 249 } else { 250 // The end time was changed. 251 startMillis = startTime.toMillis(true); 252 endTime.hour = hourOfDay; 253 endTime.minute = minute; 254 endMillis = endTime.normalize(true); 255 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; 260 } 261 } 262 263 setDate(mEndDateButton, endMillis); 264 setTime(mStartTimeButton, startMillis); 265 setTime(mEndTimeButton, endMillis); 266 } 267 } 268 269 private class TimeClickListener implements View.OnClickListener { 270 private Time mTime; 271 TimeClickListener(Time time)272 public TimeClickListener(Time time) { 273 mTime = time; 274 } 275 onClick(View v)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(); 280 } 281 } 282 283 private class DateListener implements OnDateSetListener { 284 View mView; 285 DateListener(View view)286 public DateListener(View view) { 287 mView = view; 288 } 289 onDateSet(DatePicker view, int year, int month, int monthDay)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; 294 295 // Cache the start and end millis so that we limit the number 296 // of calls to normalize() and toMillis(), which are fairly 297 // expensive. 298 long startMillis; 299 long endMillis; 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; 305 306 startTime.year = year; 307 startTime.month = month; 308 startTime.monthDay = monthDay; 309 startMillis = startTime.normalize(true); 310 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); 316 317 // If the start date has changed then update the repeats. 318 populateRepeats(); 319 } else { 320 // The end date was changed. 321 startMillis = startTime.toMillis(true); 322 endTime.year = year; 323 endTime.month = month; 324 endTime.monthDay = monthDay; 325 endMillis = endTime.normalize(true); 326 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; 331 } 332 } 333 334 setDate(mStartDateButton, startMillis); 335 setDate(mEndDateButton, endMillis); 336 setTime(mEndTimeButton, endMillis); // In case end time had to be reset 337 } 338 } 339 340 private class DateClickListener implements View.OnClickListener { 341 private Time mTime; 342 DateClickListener(Time time)343 public DateClickListener(Time time) { 344 mTime = time; 345 } 346 onClick(View v)347 public void onClick(View v) { 348 new DatePickerDialog(EditEvent.this, new DateListener(v), mTime.year, 349 mTime.month, mTime.monthDay).show(); 350 } 351 } 352 353 private class CalendarsAdapter extends ResourceCursorAdapter { CalendarsAdapter(Context context, Cursor c)354 public CalendarsAdapter(Context context, Cursor c) { 355 super(context, R.layout.calendars_item, c); 356 setDropDownViewResource(R.layout.calendars_dropdown_item); 357 } 358 359 @Override bindView(View view, Context context, Cursor cursor)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)); 363 } 364 } 365 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. onClick(View v)369 public void onClick(View v) { 370 if (v == mSaveButton) { 371 if (save()) { 372 finish(); 373 } 374 return; 375 } 376 377 if (v == mDeleteButton) { 378 long begin = mStartTime.toMillis(false /* use isDst */); 379 long end = mEndTime.toMillis(false /* use isDst */); 380 int which = -1; 381 switch (mModification) { 382 case MODIFY_SELECTED: 383 which = DeleteEventHelper.DELETE_SELECTED; 384 break; 385 case MODIFY_ALL_FOLLOWING: 386 which = DeleteEventHelper.DELETE_ALL_FOLLOWING; 387 break; 388 case MODIFY_ALL: 389 which = DeleteEventHelper.DELETE_ALL; 390 break; 391 } 392 mDeleteEventHelper.delete(begin, end, mEventCursor, which); 393 return; 394 } 395 396 if (v == mDiscardButton) { 397 finish(); 398 return; 399 } 400 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(); 407 } 408 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. onCancel(DialogInterface dialog)415 public void onCancel(DialogInterface dialog) { 416 if (dialog == mLoadingCalendarsDialog) { 417 mSaveAfterQueryComplete = false; 418 } else if (dialog == mNoCalendarsDialog) { 419 finish(); 420 } 421 } 422 423 // This is called if the user clicks on a dialog button. onClick(DialogInterface dialog, int which)424 public void onClick(DialogInterface dialog, int which) { 425 if (dialog == mNoCalendarsDialog) { 426 finish(); 427 } 428 } 429 430 private class QueryHandler extends AsyncQueryHandler { QueryHandler(ContentResolver cr)431 public QueryHandler(ContentResolver cr) { 432 super(cr); 433 } 434 435 @Override onQueryComplete(int token, Object cookie, Cursor cursor)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. 439 if (isFinishing()) { 440 stopManagingCursor(cursor); 441 cursor.close(); 442 } else { 443 mCalendarsCursor = cursor; 444 startManagingCursor(cursor); 445 446 // Stop the spinner 447 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS, 448 Window.PROGRESS_VISIBILITY_OFF); 449 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(); 456 } 457 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(); 467 return; 468 } 469 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(); 476 save(); 477 finish(); 478 } 479 } 480 } 481 } 482 483 @Override onCreate(Bundle icicle)484 protected void onCreate(Bundle icicle) { 485 super.onCreate(icicle); 486 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 487 setContentView(R.layout.edit_event); 488 489 mFirstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek(); 490 491 mStartTime = new Time(); 492 mEndTime = new Time(); 493 494 Intent intent = getIntent(); 495 mUri = intent.getData(); 496 497 if (mUri != null) { 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. 501 finish(); 502 return; 503 } 504 } 505 506 long begin = intent.getLongExtra(EVENT_BEGIN_TIME, 0); 507 long end = intent.getLongExtra(EVENT_END_TIME, 0); 508 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); 517 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); 526 } else { 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); 530 531 // Start the spinner 532 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS, 533 Window.PROGRESS_VISIBILITY_ON); 534 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 */); 539 } 540 541 // If the event is all-day, read the times in UTC timezone 542 if (begin != 0) { 543 if (allDay) { 544 String tz = mStartTime.timezone; 545 mStartTime.timezone = Time.TIMEZONE_UTC; 546 mStartTime.set(begin); 547 mStartTime.timezone = tz; 548 549 // Calling normalize to calculate isDst 550 mStartTime.normalize(true); 551 } else { 552 mStartTime.set(begin); 553 } 554 } 555 556 if (end != 0) { 557 if (allDay) { 558 String tz = mStartTime.timezone; 559 mEndTime.timezone = Time.TIMEZONE_UTC; 560 mEndTime.set(end); 561 mEndTime.timezone = tz; 562 563 // Calling normalize to calculate isDst 564 mEndTime.normalize(true); 565 } else { 566 mEndTime.set(end); 567 } 568 } 569 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); 586 587 mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 588 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 589 if (isChecked) { 590 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 591 mEndTime.monthDay--; 592 long endMillis = mEndTime.normalize(true); 593 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); 598 } 599 setDate(mEndDateButton, endMillis); 600 setTime(mEndTimeButton, endMillis); 601 } 602 603 mStartTimeButton.setVisibility(View.GONE); 604 mEndTimeButton.setVisibility(View.GONE); 605 } else { 606 if (mEndTime.hour == 0 && mEndTime.minute == 0) { 607 mEndTime.monthDay++; 608 long endMillis = mEndTime.normalize(true); 609 setDate(mEndDateButton, endMillis); 610 setTime(mEndTimeButton, endMillis); 611 } 612 613 mStartTimeButton.setVisibility(View.VISIBLE); 614 mEndTimeButton.setVisibility(View.VISIBLE); 615 } 616 } 617 }); 618 619 if (allDay) { 620 mAllDayCheckBox.setChecked(true); 621 } else { 622 mAllDayCheckBox.setChecked(false); 623 } 624 625 mSaveButton = (Button) findViewById(R.id.save); 626 mSaveButton.setOnClickListener(this); 627 628 mDeleteButton = (Button) findViewById(R.id.delete); 629 mDeleteButton.setOnClickListener(this); 630 631 mDiscardButton = (Button) findViewById(R.id.discard); 632 mDiscardButton.setOnClickListener(this); 633 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])); 641 } 642 mReminderValues = list; 643 String[] labels = r.getStringArray(R.array.reminder_minutes_labels); 644 mReminderLabels = new ArrayList<String>(Arrays.asList(labels)); 645 646 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 647 String durationString = 648 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0"); 649 mDefaultReminderMinutes = Integer.parseInt(durationString); 650 651 // Reminders cursor 652 boolean hasAlarm = (mEventCursor != null) 653 && (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0); 654 if (hasAlarm) { 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); 660 try { 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); 666 } 667 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); 675 } 676 } finally { 677 reminderCursor.close(); 678 } 679 } 680 updateRemindersVisibility(); 681 682 // Setup the + Add Reminder Button 683 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { 684 public void onClick(View v) { 685 addReminder(); 686 } 687 }; 688 ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add); 689 reminderRemoveButton.setOnClickListener(addReminderOnClickListener); 690 691 mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */); 692 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); 697 } 698 } 699 initFromIntent(Intent intent)700 private void initFromIntent(Intent intent) { 701 String title = intent.getStringExtra(Events.TITLE); 702 if (title != null) { 703 mTitleTextView.setText(title); 704 } 705 706 String location = intent.getStringExtra(Events.EVENT_LOCATION); 707 if (location != null) { 708 mLocationTextView.setText(location); 709 } 710 711 String description = intent.getStringExtra(Events.DESCRIPTION); 712 if (description != null) { 713 mDescriptionTextView.setText(description); 714 } 715 716 int availability = intent.getIntExtra(Events.TRANSPARENCY, -1); 717 if (availability != -1) { 718 mAvailabilitySpinner.setSelection(availability); 719 } 720 721 int visibility = intent.getIntExtra(Events.VISIBILITY, -1); 722 if (visibility != -1) { 723 mVisibilitySpinner.setSelection(visibility); 724 } 725 726 String rrule = intent.getStringExtra(Events.RRULE); 727 if (rrule != null) { 728 mRrule = rrule; 729 mEventRecurrence.parse(rrule); 730 } 731 } 732 733 @Override onResume()734 protected void onResume() { 735 super.onResume(); 736 737 if (mUri != null) { 738 if (mEventCursor == null || mEventCursor.getCount() == 0) { 739 // The cursor is empty. This can happen if the event was deleted. 740 finish(); 741 return; 742 } 743 } 744 745 if (mEventCursor != null) { 746 Cursor cursor = mEventCursor; 747 cursor.moveToFirst(); 748 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. 757 visibility--; 758 } 759 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); 765 766 // If we haven't synced this repeating event yet, then don't 767 // allow the user to change just one instance. 768 int itemIndex = 0; 769 CharSequence[] items; 770 if (mSyncId == null) { 771 items = new CharSequence[2]; 772 } else { 773 items = new CharSequence[3]; 774 items[itemIndex++] = getText(R.string.modify_event); 775 } 776 items[itemIndex++] = getText(R.string.modify_all); 777 items[itemIndex++] = getText(R.string.modify_all_following); 778 779 // Display the modification dialog. 780 new AlertDialog.Builder(this) 781 .setOnCancelListener(new OnCancelListener() { 782 public void onCancel(DialogInterface dialog) { 783 finish(); 784 } 785 }) 786 .setTitle(R.string.edit_event_label) 787 .setItems(items, new OnClickListener() { 788 public void onClick(DialogInterface dialog, int which) { 789 if (which == 0) { 790 mModification = 791 (mSyncId == null) ? MODIFY_ALL : MODIFY_SELECTED; 792 } else if (which == 1) { 793 mModification = 794 (mSyncId == null) ? MODIFY_ALL_FOLLOWING : MODIFY_ALL; 795 } else if (which == 2) { 796 mModification = MODIFY_ALL_FOLLOWING; 797 } 798 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); 806 } 807 } 808 }) 809 .show(); 810 } 811 812 mTitleTextView.setText(title); 813 mLocationTextView.setText(location); 814 mDescriptionTextView.setText(description); 815 mAvailabilitySpinner.setSelection(availability); 816 mVisibilitySpinner.setSelection(visibility); 817 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(); 824 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; 830 } else { 831 mStartTime.minute = 0; 832 mStartTime.hour += 1; 833 } 834 835 long startMillis = mStartTime.normalize(true /* ignore isDst */); 836 mEndTime.set(startMillis + DateUtils.HOUR_IN_MILLIS); 837 } else { 838 // New event - set the default reminder 839 if (mDefaultReminderMinutes != 0) { 840 addReminder(this, this, mReminderItems, mReminderValues, 841 mReminderLabels, mDefaultReminderMinutes); 842 } 843 844 // Hide delete button 845 mDeleteButton.setVisibility(View.GONE); 846 } 847 848 updateRemindersVisibility(); 849 populateWhen(); 850 populateRepeats(); 851 } 852 853 @Override onCreateOptionsMenu(Menu menu)854 public boolean onCreateOptionsMenu(Menu menu) { 855 MenuItem item; 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'); 860 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); 867 868 return super.onCreateOptionsMenu(menu); 869 } 870 871 @Override onPrepareOptionsMenu(Menu 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); 876 } else { 877 menu.setGroupVisible(MENU_GROUP_REMINDER, false); 878 menu.setGroupEnabled(MENU_GROUP_REMINDER, false); 879 } 880 881 if (mExtraOptions.getVisibility() == View.VISIBLE) { 882 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, false); 883 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, true); 884 } else { 885 menu.setGroupVisible(MENU_GROUP_SHOW_OPTIONS, true); 886 menu.setGroupVisible(MENU_GROUP_HIDE_OPTIONS, false); 887 } 888 889 return super.onPrepareOptionsMenu(menu); 890 } 891 addReminder()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 */); 898 } else { 899 addReminder(this, this, mReminderItems, mReminderValues, 900 mReminderLabels, mDefaultReminderMinutes); 901 } 902 updateRemindersVisibility(); 903 } 904 905 @Override onOptionsItemSelected(MenuItem item)906 public boolean onOptionsItemSelected(MenuItem item) { 907 switch (item.getItemId()) { 908 case MENU_ADD_REMINDER: 909 addReminder(); 910 return true; 911 case MENU_SHOW_EXTRA_OPTIONS: 912 mExtraOptions.setVisibility(View.VISIBLE); 913 return true; 914 case MENU_HIDE_EXTRA_OPTIONS: 915 mExtraOptions.setVisibility(View.GONE); 916 return true; 917 } 918 return super.onOptionsItemSelected(item); 919 } 920 921 @Override onKeyDown(int keyCode, KeyEvent event)922 public boolean onKeyDown(int keyCode, KeyEvent event) { 923 switch (keyCode) { 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()) { 929 if (!save()) { 930 // We cannot exit this activity because the calendars 931 // are still loading. 932 return true; 933 } 934 } 935 break; 936 } 937 938 return super.onKeyDown(keyCode, event); 939 } 940 populateWhen()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); 946 947 setTime(mStartTimeButton, startMillis); 948 setTime(mEndTimeButton, endMillis); 949 950 mStartDateButton.setOnClickListener(new DateClickListener(mStartTime)); 951 mEndDateButton.setOnClickListener(new DateClickListener(mEndTime)); 952 953 mStartTimeButton.setOnClickListener(new TimeClickListener(mStartTime)); 954 mEndTimeButton.setOnClickListener(new TimeClickListener(mEndTime)); 955 } 956 populateRepeats()957 private void populateRepeats() { 958 Time time = mStartTime; 959 Resources r = getResources(); 960 int resource = android.R.layout.simple_spinner_item; 961 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), 970 }; 971 String[] ordinals = r.getStringArray(R.array.ordinal_labels); 972 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(); 978 979 ArrayList<String> repeatArray = new ArrayList<String>(0); 980 ArrayList<Integer> recurrenceIndexes = new ArrayList<Integer>(0); 981 982 repeatArray.add(r.getString(R.string.does_not_repeat)); 983 recurrenceIndexes.add(DOES_NOT_REPEAT); 984 985 repeatArray.add(r.getString(R.string.daily)); 986 recurrenceIndexes.add(REPEATS_DAILY); 987 988 if (isWeekdayEvent) { 989 repeatArray.add(r.getString(R.string.every_weekday)); 990 recurrenceIndexes.add(REPEATS_EVERY_WEEKDAY); 991 } 992 993 String format = r.getString(R.string.weekly); 994 repeatArray.add(String.format(format, time.format("%A"))); 995 recurrenceIndexes.add(REPEATS_WEEKLY_ON_DAY); 996 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); 1002 1003 format = r.getString(R.string.monthly_on_day); 1004 repeatArray.add(String.format(format, time.monthDay)); 1005 recurrenceIndexes.add(REPEATS_MONTHLY_ON_DAY); 1006 1007 long when = time.toMillis(false); 1008 format = r.getString(R.string.yearly); 1009 int flags = 0; 1010 if (DateFormat.is24HourFormat(this)) { 1011 flags |= DateUtils.FORMAT_24HOUR; 1012 } 1013 repeatArray.add(String.format(format, DateUtils.formatDateTime(this, when, flags))); 1014 recurrenceIndexes.add(REPEATS_YEARLY); 1015 1016 if (isCustomRecurrence) { 1017 repeatArray.add(r.getString(R.string.custom)); 1018 recurrenceIndexes.add(REPEATS_CUSTOM); 1019 } 1020 mRecurrenceIndexes = recurrenceIndexes; 1021 1022 int position = recurrenceIndexes.indexOf(DOES_NOT_REPEAT); 1023 if (mRrule != null) { 1024 if (isCustomRecurrence) { 1025 position = recurrenceIndexes.indexOf(REPEATS_CUSTOM); 1026 } else { 1027 switch (mEventRecurrence.freq) { 1028 case EventRecurrence.DAILY: 1029 position = recurrenceIndexes.indexOf(REPEATS_DAILY); 1030 break; 1031 case EventRecurrence.WEEKLY: 1032 if (mEventRecurrence.repeatsOnEveryWeekDay()) { 1033 position = recurrenceIndexes.indexOf(REPEATS_EVERY_WEEKDAY); 1034 } else { 1035 position = recurrenceIndexes.indexOf(REPEATS_WEEKLY_ON_DAY); 1036 } 1037 break; 1038 case EventRecurrence.MONTHLY: 1039 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 1040 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY_COUNT); 1041 } else { 1042 position = recurrenceIndexes.indexOf(REPEATS_MONTHLY_ON_DAY); 1043 } 1044 break; 1045 case EventRecurrence.YEARLY: 1046 position = recurrenceIndexes.indexOf(REPEATS_YEARLY); 1047 break; 1048 } 1049 } 1050 } 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); 1055 } 1056 1057 // Adds a reminder to the displayed list of reminders. 1058 // Returns true if successfully added reminder, false if no reminders can 1059 // be added. addReminder(Activity activity, View.OnClickListener listener, ArrayList<LinearLayout> items, ArrayList<Integer> values, ArrayList<String> labels, int minutes)1060 static boolean addReminder(Activity activity, View.OnClickListener listener, 1061 ArrayList<LinearLayout> items, ArrayList<Integer> values, 1062 ArrayList<String> labels, int minutes) { 1063 1064 if (items.size() >= MAX_REMINDERS) { 1065 return false; 1066 } 1067 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); 1072 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); 1080 1081 ImageButton reminderRemoveButton; 1082 reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove); 1083 reminderRemoveButton.setOnClickListener(listener); 1084 1085 int index = findMinutesInReminderList(values, minutes); 1086 spinner.setSelection(index); 1087 items.add(reminderItem); 1088 1089 return true; 1090 } 1091 addMinutesToList(Context context, ArrayList<Integer> values, ArrayList<String> labels, int minutes)1092 static void addMinutesToList(Context context, ArrayList<Integer> values, 1093 ArrayList<String> labels, int minutes) { 1094 int index = values.indexOf(minutes); 1095 if (index != -1) { 1096 return; 1097 } 1098 1099 // The requested "minutes" does not exist in the list, so insert it 1100 // into the list. 1101 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); 1108 return; 1109 } 1110 } 1111 1112 values.add(minutes); 1113 labels.add(len, label); 1114 } 1115 1116 /** 1117 * Finds the index of the given "minutes" in the "values" list. 1118 * 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 1122 */ findMinutesInReminderList(ArrayList<Integer> values, int minutes)1123 private static int findMinutesInReminderList(ArrayList<Integer> values, int minutes) { 1124 int index = values.indexOf(minutes); 1125 if (index == -1) { 1126 // This should never happen. 1127 Log.e("Cal", "Cannot find minutes (" + minutes + ") in list"); 1128 return 0; 1129 } 1130 return index; 1131 } 1132 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 1136 // "2 hours". constructReminderLabel(Context context, int minutes, boolean abbrev)1137 static String constructReminderLabel(Context context, int minutes, boolean abbrev) { 1138 Resources resources = context.getResources(); 1139 int value, resId; 1140 1141 if (minutes % 60 != 0) { 1142 value = minutes; 1143 if (abbrev) { 1144 resId = R.plurals.Nmins; 1145 } else { 1146 resId = R.plurals.Nminutes; 1147 } 1148 } else if (minutes % (24 * 60) != 0) { 1149 value = minutes / 60; 1150 resId = R.plurals.Nhours; 1151 } else { 1152 value = minutes / ( 24 * 60); 1153 resId = R.plurals.Ndays; 1154 } 1155 1156 String format = resources.getQuantityString(resId, value); 1157 return String.format(format, value); 1158 } 1159 updateRemindersVisibility()1160 private void updateRemindersVisibility() { 1161 if (mReminderItems.size() == 0) { 1162 mRemindersSeparator.setVisibility(View.GONE); 1163 mRemindersContainer.setVisibility(View.GONE); 1164 } else { 1165 mRemindersSeparator.setVisibility(View.VISIBLE); 1166 mRemindersContainer.setVisibility(View.VISIBLE); 1167 } 1168 } 1169 setDate(TextView view, long millis)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)); 1175 } 1176 setTime(TextView view, long millis)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; 1181 } 1182 view.setText(DateUtils.formatDateTime(this, millis, flags)); 1183 } 1184 1185 // Saves the event. Returns true if it is okay to exit this activity. save()1186 private boolean save() { 1187 boolean forceSaveReminders = false; 1188 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), 1199 true, true, this); 1200 mSaveAfterQueryComplete = true; 1201 } 1202 return false; 1203 } 1204 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 1207 // has a calendar. 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."); 1211 return true; 1212 } 1213 Toast.makeText(this, R.string.creating_event, Toast.LENGTH_SHORT).show(); 1214 } else { 1215 Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show(); 1216 } 1217 1218 ContentResolver cr = getContentResolver(); 1219 ContentValues values = getContentValuesFromUi(); 1220 Uri uri = mUri; 1221 1222 // For recurring events, we must make sure that we use duration rather 1223 // than dtend. 1224 if (uri == null) { 1225 // Create new event with new contents 1226 addRecurrenceRule(values); 1227 uri = cr.insert(Events.CONTENT_URI, values); 1228 forceSaveReminders = true; 1229 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); 1235 1236 } else if (mInitialValues.getAsString(Events.RRULE) == null) { 1237 // This event was changed from a non-repeating event to a 1238 // repeating event. 1239 addRecurrenceRule(values); 1240 values.remove(Events.DTEND); 1241 cr.update(uri, values, null, null); 1242 1243 } else if (mModification == MODIFY_SELECTED) { 1244 // Modify contents of the current instance of repeating event 1245 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); 1252 1253 uri = cr.insert(Events.CONTENT_URI, values); 1254 forceSaveReminders = true; 1255 1256 } else if (mModification == MODIFY_ALL_FOLLOWING) { 1257 // Modify this instance and all future instances of repeating event 1258 addRecurrenceRule(values); 1259 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); 1267 } else { 1268 // Update the current repeating event to end at the new 1269 // start time. 1270 updatePastEvents(cr, uri); 1271 } 1272 uri = cr.insert(Events.CONTENT_URI, values); 1273 } else { 1274 if (isFirstEventInSeries()) { 1275 checkTimeDependentFields(values); 1276 values.remove(Events.DTEND); 1277 cr.update(uri, values, null, null); 1278 } else { 1279 // Update the current repeating event to end at the new 1280 // start time. 1281 updatePastEvents(cr, uri); 1282 1283 // Create a new event with the user-modified fields 1284 values.remove(Events.DTEND); 1285 uri = cr.insert(Events.CONTENT_URI, values); 1286 } 1287 } 1288 forceSaveReminders = true; 1289 1290 } else if (mModification == MODIFY_ALL) { 1291 1292 // Modify all instances of repeating event 1293 addRecurrenceRule(values); 1294 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; 1302 } else { 1303 checkTimeDependentFields(values); 1304 values.remove(Events.DTEND); 1305 cr.update(uri, values, null, null); 1306 } 1307 } 1308 1309 if (uri != null) { 1310 long eventId = ContentUris.parseId(uri); 1311 ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems, 1312 mReminderValues); 1313 saveReminders(cr, eventId, reminderMinutes, mOriginalMinutes, 1314 forceSaveReminders); 1315 } 1316 return true; 1317 } 1318 isFirstEventInSeries()1319 private boolean isFirstEventInSeries() { 1320 int dtStart = mEventCursor.getColumnIndexOrThrow(Events.DTSTART); 1321 long start = mEventCursor.getLong(dtStart); 1322 return start == mStartTime.toMillis(true); 1323 } 1324 updatePastEvents(ContentResolver cr, Uri uri)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); 1331 1332 Time untilTime = new Time(); 1333 long begin = mInitialValues.getAsLong(EVENT_BEGIN_TIME); 1334 ContentValues oldValues = new ContentValues(); 1335 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; 1341 1342 // Subtract one second from the old begin time to get the new 1343 // "until" time. 1344 untilTime.set(begin - 1000); // subtract one second (1000 millis) 1345 if (allDay) { 1346 untilTime.hour = 0; 1347 untilTime.minute = 0; 1348 untilTime.second = 0; 1349 untilTime.allDay = true; 1350 untilTime.normalize(false); 1351 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"; 1360 } 1361 } 1362 mEventRecurrence.until = untilTime.format2445(); 1363 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); 1368 } 1369 checkTimeDependentFields(ContentValues values)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); 1376 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); 1382 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); 1393 return; 1394 } 1395 1396 if (oldRrule == null || newRrule == null) { 1397 return; 1398 } 1399 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; 1413 } 1414 values.put(Events.DTSTART, oldStartMillis); 1415 } 1416 } 1417 reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems, ArrayList<Integer> reminderValues)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); 1427 } 1428 return reminderMinutes; 1429 } 1430 1431 /** 1432 * Saves the reminders, if they changed. Returns true if the database 1433 * was updated. 1434 * 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 1440 * change 1441 * @return true if the database was updated 1442 */ saveReminders(ContentResolver cr, long eventId, ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes, boolean forceSave)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) { 1448 return false; 1449 } 1450 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); 1455 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 */); 1462 1463 // Insert the new reminders, if any 1464 for (int i = 0; i < len; i++) { 1465 int minutes = reminderMinutes.get(i); 1466 1467 values.clear(); 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); 1472 } 1473 return true; 1474 } 1475 addRecurrenceRule(ContentValues values)1476 private void addRecurrenceRule(ContentValues values) { 1477 updateRecurrenceRule(); 1478 1479 if (mRrule == null) { 1480 return; 1481 } 1482 1483 values.put(Events.RRULE, mRrule); 1484 long end = mEndTime.toMillis(true /* ignore dst */); 1485 long start = mStartTime.toMillis(true /* ignore dst */); 1486 String duration; 1487 1488 boolean isAllDay = mAllDayCheckBox.isChecked(); 1489 if (isAllDay) { 1490 long days = (end - start + DateUtils.DAY_IN_MILLIS - 1) / DateUtils.DAY_IN_MILLIS; 1491 duration = "P" + days + "D"; 1492 } else { 1493 long seconds = (end - start) / DateUtils.SECOND_IN_MILLIS; 1494 duration = "P" + seconds + "S"; 1495 } 1496 values.put(Events.DURATION, duration); 1497 } 1498 updateRecurrenceRule()1499 private void updateRecurrenceRule() { 1500 int position = mRepeatsSpinner.getSelectedItemPosition(); 1501 int selection = mRecurrenceIndexes.get(position); 1502 1503 if (selection == DOES_NOT_REPEAT) { 1504 mRrule = null; 1505 return; 1506 } else if (selection == REPEATS_CUSTOM) { 1507 // Keep custom recurrence as before. 1508 return; 1509 } else if (selection == REPEATS_DAILY) { 1510 mEventRecurrence.freq = EventRecurrence.DAILY; 1511 } else if (selection == REPEATS_EVERY_WEEKDAY) { 1512 mEventRecurrence.freq = EventRecurrence.WEEKLY; 1513 int dayCount = 5; 1514 int[] byday = new int[dayCount]; 1515 int[] bydayNum = new int[dayCount]; 1516 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++) { 1523 bydayNum[day] = 0; 1524 } 1525 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]; 1532 int dayCount = 1; 1533 int[] dayNum = new int[dayCount]; 1534 1535 days[0] = EventRecurrence.timeDay2Day(mStartTime.weekDay); 1536 // not sure why this needs to be zero, but set it for now. 1537 dayNum[0] = 0; 1538 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; 1553 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) { 1559 dayCount = -1; 1560 } 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; 1567 } 1568 1569 // Set the week start day. 1570 mEventRecurrence.wkst = EventRecurrence.calendarDay2Day(mFirstDayOfWeek); 1571 mRrule = mEventRecurrence.toString(); 1572 } 1573 getContentValuesFromUi()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(); 1579 1580 ContentValues values = new ContentValues(); 1581 1582 String timezone = null; 1583 long startMillis; 1584 long endMillis; 1585 long calendarId; 1586 if (isAllDay) { 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); 1595 1596 mEndTime.hour = 0; 1597 mEndTime.minute = 0; 1598 mEndTime.second = 0; 1599 mEndTime.monthDay++; 1600 mEndTime.timezone = timezone; 1601 endMillis = mEndTime.normalize(true); 1602 1603 if (mEventCursor == null) { 1604 // This is a new event 1605 calendarId = mCalendarsSpinner.getSelectedItemId(); 1606 } else { 1607 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID); 1608 } 1609 } else { 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); 1615 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(); 1621 } 1622 calendarId = mInitialValues.getAsLong(Events.CALENDAR_ID); 1623 } else { 1624 // This is a new event 1625 calendarId = mCalendarsSpinner.getSelectedItemId(); 1626 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(); 1630 } 1631 } 1632 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()); 1642 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. 1646 visibility++; 1647 } 1648 values.put(Events.VISIBILITY, visibility); 1649 1650 return values; 1651 } 1652 isEmpty()1653 private boolean isEmpty() { 1654 String title = mTitleTextView.getText().toString(); 1655 if (title.length() > 0) { 1656 return false; 1657 } 1658 1659 String location = mLocationTextView.getText().toString(); 1660 if (location.length() > 0) { 1661 return false; 1662 } 1663 1664 String description = mDescriptionTextView.getText().toString(); 1665 if (description.length() > 0) { 1666 return false; 1667 } 1668 1669 return true; 1670 } 1671 isCustomRecurrence()1672 private boolean isCustomRecurrence() { 1673 1674 if (mEventRecurrence.until != null || mEventRecurrence.interval != 0) { 1675 return true; 1676 } 1677 1678 if (mEventRecurrence.freq == 0) { 1679 return false; 1680 } 1681 1682 switch (mEventRecurrence.freq) { 1683 case EventRecurrence.DAILY: 1684 return false; 1685 case EventRecurrence.WEEKLY: 1686 if (mEventRecurrence.repeatsOnEveryWeekDay() && isWeekdayEvent()) { 1687 return false; 1688 } else if (mEventRecurrence.bydayCount == 1) { 1689 return false; 1690 } 1691 break; 1692 case EventRecurrence.MONTHLY: 1693 if (mEventRecurrence.repeatsMonthlyOnDayCount()) { 1694 return false; 1695 } else if (mEventRecurrence.bydayCount == 0 && mEventRecurrence.bymonthdayCount == 1) { 1696 return false; 1697 } 1698 break; 1699 case EventRecurrence.YEARLY: 1700 return false; 1701 } 1702 1703 return true; 1704 } 1705 isWeekdayEvent()1706 private boolean isWeekdayEvent() { 1707 if (mStartTime.weekDay != Time.SUNDAY && mStartTime.weekDay != Time.SATURDAY) { 1708 return true; 1709 } 1710 return false; 1711 } 1712 } 1713