EventInfoFragment.java revision 1121e409c5f504e8df75982475d8cc607d5f0dfa
1/* 2 * Copyright (C) 2010 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 17package com.android.calendar; 18 19import com.android.calendar.CalendarController.EventInfo; 20import com.android.calendar.CalendarController.EventType; 21import com.android.calendar.CalendarEventModel.Attendee; 22import com.android.calendar.CalendarEventModel.ReminderEntry; 23import com.android.calendar.event.AttendeesView; 24import com.android.calendar.event.EditEventHelper; 25import com.android.calendarcommon.EventRecurrence; 26import com.android.calendar.event.EventViewUtils; 27 28import android.app.Activity; 29import android.app.Dialog; 30import android.app.DialogFragment; 31import android.app.Service; 32import android.content.ActivityNotFoundException; 33import android.content.ContentProviderOperation; 34import android.content.ContentResolver; 35import android.content.ContentUris; 36import android.content.ContentValues; 37import android.content.Context; 38import android.content.Intent; 39import android.content.SharedPreferences; 40import android.content.res.Resources; 41import android.database.Cursor; 42import android.graphics.Rect; 43import android.graphics.Typeface; 44import android.net.Uri; 45import android.os.Bundle; 46import android.provider.CalendarContract; 47import android.provider.CalendarContract.Attendees; 48import android.provider.CalendarContract.Calendars; 49import android.provider.CalendarContract.Events; 50import android.provider.CalendarContract.Reminders; 51import android.provider.ContactsContract; 52import android.provider.ContactsContract.CommonDataKinds; 53import android.provider.ContactsContract.Intents; 54import android.provider.ContactsContract.QuickContact; 55import android.text.Spannable; 56import android.text.SpannableStringBuilder; 57import android.text.TextUtils; 58import android.text.format.DateFormat; 59import android.text.format.DateUtils; 60import android.text.format.Time; 61import android.text.style.ForegroundColorSpan; 62import android.text.style.StrikethroughSpan; 63import android.text.style.StyleSpan; 64import android.text.util.Linkify; 65import android.text.util.Rfc822Token; 66import android.util.Log; 67import android.view.Gravity; 68import android.view.LayoutInflater; 69import android.view.Menu; 70import android.view.MenuInflater; 71import android.view.MenuItem; 72import android.view.MotionEvent; 73import android.view.View; 74import android.view.View.OnClickListener; 75import android.view.View.OnTouchListener; 76import android.view.ViewGroup; 77import android.view.Window; 78import android.view.WindowManager; 79import android.view.accessibility.AccessibilityEvent; 80import android.view.accessibility.AccessibilityManager; 81import android.widget.AdapterView; 82import android.widget.Button; 83import android.widget.ImageButton; 84import android.widget.LinearLayout; 85import android.widget.RadioButton; 86import android.widget.RadioGroup; 87import android.widget.ScrollView; 88import android.widget.RadioGroup.OnCheckedChangeListener; 89import android.widget.TextView; 90import android.widget.Toast; 91 92import java.util.ArrayList; 93import java.util.Arrays; 94import java.util.Collections; 95import java.util.Formatter; 96import java.util.List; 97import java.util.Locale; 98import java.util.regex.Pattern; 99import java.util.TimeZone; 100 101 102public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener, 103 CalendarController.EventHandler, OnClickListener { 104 public static final boolean DEBUG = false; 105 106 public static final String TAG = "EventInfoFragment"; 107 108 protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; 109 protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis"; 110 protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis"; 111 protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog"; 112 protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response"; 113 114 private static final String PERIOD_SPACE = ". "; 115 116 /** 117 * These are the corresponding indices into the array of strings 118 * "R.array.change_response_labels" in the resource file. 119 */ 120 static final int UPDATE_SINGLE = 0; 121 static final int UPDATE_ALL = 1; 122 123 // Query tokens for QueryHandler 124 private static final int TOKEN_QUERY_EVENT = 1 << 0; 125 private static final int TOKEN_QUERY_CALENDARS = 1 << 1; 126 private static final int TOKEN_QUERY_ATTENDEES = 1 << 2; 127 private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3; 128 private static final int TOKEN_QUERY_REMINDERS = 1 << 4; 129 private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS 130 | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT 131 | TOKEN_QUERY_REMINDERS; 132 private int mCurrentQuery = 0; 133 134 private static final String[] EVENT_PROJECTION = new String[] { 135 Events._ID, // 0 do not remove; used in DeleteEventHelper 136 Events.TITLE, // 1 do not remove; used in DeleteEventHelper 137 Events.RRULE, // 2 do not remove; used in DeleteEventHelper 138 Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper 139 Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper 140 Events.DTSTART, // 5 do not remove; used in DeleteEventHelper 141 Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper 142 Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper 143 Events.DESCRIPTION, // 8 144 Events.EVENT_LOCATION, // 9 145 Calendars.CALENDAR_ACCESS_LEVEL, // 10 146 Calendars.CALENDAR_COLOR, // 11 147 Events.HAS_ATTENDEE_DATA, // 12 148 Events.ORGANIZER, // 13 149 Events.HAS_ALARM, // 14 150 Calendars.MAX_REMINDERS, //15 151 Calendars.ALLOWED_REMINDERS, // 16 152 Events.ORIGINAL_SYNC_ID // 17 do not remove; used in DeleteEventHelper 153 }; 154 private static final int EVENT_INDEX_ID = 0; 155 private static final int EVENT_INDEX_TITLE = 1; 156 private static final int EVENT_INDEX_RRULE = 2; 157 private static final int EVENT_INDEX_ALL_DAY = 3; 158 private static final int EVENT_INDEX_CALENDAR_ID = 4; 159 private static final int EVENT_INDEX_SYNC_ID = 6; 160 private static final int EVENT_INDEX_EVENT_TIMEZONE = 7; 161 private static final int EVENT_INDEX_DESCRIPTION = 8; 162 private static final int EVENT_INDEX_EVENT_LOCATION = 9; 163 private static final int EVENT_INDEX_ACCESS_LEVEL = 10; 164 private static final int EVENT_INDEX_COLOR = 11; 165 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12; 166 private static final int EVENT_INDEX_ORGANIZER = 13; 167 private static final int EVENT_INDEX_HAS_ALARM = 14; 168 private static final int EVENT_INDEX_MAX_REMINDERS = 15; 169 private static final int EVENT_INDEX_ALLOWED_REMINDERS = 16; 170 171 172 private static final String[] ATTENDEES_PROJECTION = new String[] { 173 Attendees._ID, // 0 174 Attendees.ATTENDEE_NAME, // 1 175 Attendees.ATTENDEE_EMAIL, // 2 176 Attendees.ATTENDEE_RELATIONSHIP, // 3 177 Attendees.ATTENDEE_STATUS, // 4 178 }; 179 private static final int ATTENDEES_INDEX_ID = 0; 180 private static final int ATTENDEES_INDEX_NAME = 1; 181 private static final int ATTENDEES_INDEX_EMAIL = 2; 182 private static final int ATTENDEES_INDEX_RELATIONSHIP = 3; 183 private static final int ATTENDEES_INDEX_STATUS = 4; 184 185 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; 186 187 private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, " 188 + Attendees.ATTENDEE_EMAIL + " ASC"; 189 190 private static final String[] REMINDERS_PROJECTION = new String[] { 191 Reminders._ID, // 0 192 Reminders.MINUTES, // 1 193 Reminders.METHOD // 2 194 }; 195 private static final int REMINDERS_INDEX_ID = 0; 196 private static final int REMINDERS_MINUTES_ID = 1; 197 private static final int REMINDERS_METHOD_ID = 2; 198 199 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?"; 200 201 static final String[] CALENDARS_PROJECTION = new String[] { 202 Calendars._ID, // 0 203 Calendars.CALENDAR_DISPLAY_NAME, // 1 204 Calendars.OWNER_ACCOUNT, // 2 205 Calendars.CAN_ORGANIZER_RESPOND // 3 206 }; 207 static final int CALENDARS_INDEX_DISPLAY_NAME = 1; 208 static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 209 static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3; 210 211 static final String CALENDARS_WHERE = Calendars._ID + "=?"; 212 static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?"; 213 214 private View mView; 215 216 private Uri mUri; 217 private long mEventId; 218 private Cursor mEventCursor; 219 private Cursor mAttendeesCursor; 220 private Cursor mCalendarsCursor; 221 private Cursor mRemindersCursor; 222 223 private static float mScale = 0; // Used for supporting different screen densities 224 225 private long mStartMillis; 226 private long mEndMillis; 227 228 private boolean mHasAttendeeData; 229 private boolean mIsOrganizer; 230 private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 231 private boolean mOwnerCanRespond; 232 private String mCalendarOwnerAccount; 233 private boolean mCanModifyCalendar; 234 private boolean mIsBusyFreeCalendar; 235 private int mNumOfAttendees; 236 237 private EditResponseHelper mEditResponseHelper; 238 239 private int mOriginalAttendeeResponse; 240 private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE; 241 private boolean mIsRepeating; 242 private boolean mHasAlarm; 243 private int mMaxReminders; 244 private String mCalendarAllowedReminders; 245 246 private TextView mTitle; 247 private TextView mWhen; 248 private TextView mWhere; 249 private TextView mWhat; 250 private TextView mAttendees; 251 private AttendeesView mLongAttendees; 252 private Menu mMenu; 253 private View mHeadlines; 254 private ScrollView mScrollView; 255 256 private Pattern mWildcardPattern = Pattern.compile("^.*$"); 257 258 ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>(); 259 ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>(); 260 ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>(); 261 ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>(); 262 private int mColor; 263 264 265 private int mDefaultReminderMinutes; 266 private ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0); 267 public ArrayList<ReminderEntry> mReminders; 268 public ArrayList<ReminderEntry> mOriginalReminders; 269 270 /** 271 * Contents of the "minutes" spinner. This has default values from the XML file, augmented 272 * with any additional values that were already associated with the event. 273 */ 274 private ArrayList<Integer> mReminderMinuteValues; 275 private ArrayList<String> mReminderMinuteLabels; 276 277 /** 278 * Contents of the "methods" spinner. The "values" list specifies the method constant 279 * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels. Any methods that 280 * aren't allowed by the Calendar will be removed. 281 */ 282 private ArrayList<Integer> mReminderMethodValues; 283 private ArrayList<String> mReminderMethodLabels; 284 285 286 287 private QueryHandler mHandler; 288 289 private Runnable mTZUpdater = new Runnable() { 290 @Override 291 public void run() { 292 updateEvent(mView); 293 } 294 }; 295 296 private static int DIALOG_WIDTH = 500; 297 private static int DIALOG_HEIGHT = 600; 298 private boolean mIsDialog = false; 299 private boolean mIsPaused = true; 300 private boolean mDismissOnResume = false; 301 private int mX = -1; 302 private int mY = -1; 303 private Button mDescButton; // Button to expand/collapse the description 304 private String mMoreLabel; // Labels for the button 305 private String mLessLabel; 306 private boolean mShowMaxDescription; // Current status of button 307 private int mDescLineNum; // The default number of lines in the description 308 private boolean mIsTabletConfig; 309 private Activity mActivity; 310 311 private class QueryHandler extends AsyncQueryService { 312 public QueryHandler(Context context) { 313 super(context); 314 } 315 316 @Override 317 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 318 // if the activity is finishing, then close the cursor and return 319 final Activity activity = getActivity(); 320 if (activity == null || activity.isFinishing()) { 321 cursor.close(); 322 return; 323 } 324 325 switch (token) { 326 case TOKEN_QUERY_EVENT: 327 mEventCursor = Utils.matrixCursorFromCursor(cursor); 328 if (initEventCursor()) { 329 // The cursor is empty. This can happen if the event was 330 // deleted. 331 // FRAG_TODO we should no longer rely on Activity.finish() 332 activity.finish(); 333 return; 334 } 335 updateEvent(mView); 336 337 // start calendar query 338 Uri uri = Calendars.CONTENT_URI; 339 String[] args = new String[] { 340 Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))}; 341 startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION, 342 CALENDARS_WHERE, args, null); 343 break; 344 case TOKEN_QUERY_CALENDARS: 345 mCalendarsCursor = Utils.matrixCursorFromCursor(cursor); 346 updateCalendar(mView); 347 // FRAG_TODO fragments shouldn't set the title anymore 348 updateTitle(); 349 // update the action bar since our option set might have changed 350 activity.invalidateOptionsMenu(); 351 352 if (!mIsBusyFreeCalendar) { 353 args = new String[] { Long.toString(mEventId) }; 354 355 // start attendees query 356 uri = Attendees.CONTENT_URI; 357 startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION, 358 ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER); 359 } else { 360 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES); 361 } 362 mOriginalReminders = new ArrayList<ReminderEntry> (); 363 if (mHasAlarm) { 364 // start reminders query 365 args = new String[] { Long.toString(mEventId) }; 366 uri = Reminders.CONTENT_URI; 367 startQuery(TOKEN_QUERY_REMINDERS, null, uri, 368 REMINDERS_PROJECTION, REMINDERS_WHERE, args, null); 369 } else { 370 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS); 371 } 372 break; 373 case TOKEN_QUERY_ATTENDEES: 374 mAttendeesCursor = Utils.matrixCursorFromCursor(cursor); 375 initAttendeesCursor(mView); 376 updateResponse(mView); 377 break; 378 case TOKEN_QUERY_REMINDERS: 379 mRemindersCursor = Utils.matrixCursorFromCursor(cursor); 380 initReminders(mView, mRemindersCursor); 381 break; 382 case TOKEN_QUERY_DUPLICATE_CALENDARS: 383 Resources res = activity.getResources(); 384 SpannableStringBuilder sb = new SpannableStringBuilder(); 385 386 // Label 387 String label = res.getString(R.string.view_event_calendar_label); 388 sb.append(label).append(" "); 389 sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(), 390 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 391 392 // Calendar display name 393 String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 394 sb.append(calendarName); 395 396 // Show email account if display name is not unique and 397 // display name != email 398 String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 399 if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) { 400 sb.append(" (").append(email).append(")"); 401 } 402 403 break; 404 } 405 cursor.close(); 406 sendAccessibilityEventIfQueryDone(token); 407 } 408 409 } 410 411 private void sendAccessibilityEventIfQueryDone(int token) { 412 mCurrentQuery |= token; 413 if (mCurrentQuery == TOKEN_QUERY_ALL) { 414 sendAccessibilityEvent(); 415 } 416 } 417 418 public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, 419 int attendeeResponse, boolean isDialog) { 420 421 if (mScale == 0) { 422 mScale = context.getResources().getDisplayMetrics().density; 423 if (mScale != 1) { 424 DIALOG_WIDTH *= mScale; 425 DIALOG_HEIGHT *= mScale; 426 } 427 } 428 mIsDialog = isDialog; 429 430 setStyle(DialogFragment.STYLE_NO_TITLE, 0); 431 mUri = uri; 432 mStartMillis = startMillis; 433 mEndMillis = endMillis; 434 mAttendeeResponseFromIntent = attendeeResponse; 435 } 436 437 // This is currently required by the fragment manager. 438 public EventInfoFragment() { 439 } 440 441 442 443 public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, 444 int attendeeResponse, boolean isDialog) { 445 this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis, 446 endMillis, attendeeResponse, isDialog); 447 mEventId = eventId; 448 } 449 450 @Override 451 public void onActivityCreated(Bundle savedInstanceState) { 452 super.onActivityCreated(savedInstanceState); 453 454 if (savedInstanceState != null) { 455 mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false); 456 } 457 458 if (mIsDialog) { 459 applyDialogParams(); 460 } 461 } 462 463 private void applyDialogParams() { 464 Dialog dialog = getDialog(); 465 dialog.setCanceledOnTouchOutside(true); 466 467 Window window = dialog.getWindow(); 468 window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 469 470 WindowManager.LayoutParams a = window.getAttributes(); 471 a.dimAmount = .4f; 472 473 a.width = DIALOG_WIDTH; 474 a.height = DIALOG_HEIGHT; 475 476 477 // On tablets , do smart positioning of dialog 478 // On phones , use the whole screen 479 480 if (mX != -1 || mY != -1) { 481 a.x = mX - a.width - 64; 482 if (a.x < 0) { 483 a.x = mX + 64; 484 } 485 a.y = mY - 64; 486 a.gravity = Gravity.LEFT | Gravity.TOP; 487 } 488 window.setAttributes(a); 489 } 490 491 public void setDialogParams(int x, int y) { 492 mX = x; 493 mY = y; 494 } 495 496 // Implements OnCheckedChangeListener 497 @Override 498 public void onCheckedChanged(RadioGroup group, int checkedId) { 499 // If this is not a repeating event, then don't display the dialog 500 // asking which events to change. 501 if (!mIsRepeating) { 502 return; 503 } 504 505 // If the selection is the same as the original, then don't display the 506 // dialog asking which events to change. 507 if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) { 508 return; 509 } 510 511 // This is a repeating event. We need to ask the user if they mean to 512 // change just this one instance or all instances. 513 mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents()); 514 } 515 516 public void onNothingSelected(AdapterView<?> parent) { 517 } 518 519 @Override 520 public void onAttach(Activity activity) { 521 super.onAttach(activity); 522 mActivity = activity; 523 mEditResponseHelper = new EditResponseHelper(activity); 524 mHandler = new QueryHandler(activity); 525 mDescLineNum = activity.getResources().getInteger((R.integer.event_info_desc_line_num)); 526 mMoreLabel = activity.getResources().getString((R.string.event_info_desc_more)); 527 mLessLabel = activity.getResources().getString((R.string.event_info_desc_less)); 528 if (!mIsDialog) { 529 setHasOptionsMenu(true); 530 } 531 } 532 533 @Override 534 public View onCreateView(LayoutInflater inflater, ViewGroup container, 535 Bundle savedInstanceState) { 536 mView = inflater.inflate(R.layout.event_info, container, false); 537 mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view); 538 mTitle = (TextView) mView.findViewById(R.id.title); 539 mWhen = (TextView) mView.findViewById(R.id.when); 540 mWhere = (TextView) mView.findViewById(R.id.where); 541 mWhat = (TextView) mView.findViewById(R.id.description); 542 mAttendees = (TextView) mView.findViewById(R.id.attendee_list); 543 mHeadlines = mView.findViewById(R.id.event_info_headline); 544 mLongAttendees = (AttendeesView)mView.findViewById(R.id.long_attendee_list); 545 mDescButton = (Button)mView.findViewById(R.id.desc_expand); 546 mDescButton.setOnClickListener(new View.OnClickListener() { 547 @Override 548 public void onClick(View v) { 549 mShowMaxDescription = !mShowMaxDescription; 550 updateDescription(); 551 } 552 }); 553 mShowMaxDescription = false; // Show short version of description as default. 554 mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config); 555 556 if (mUri == null) { 557 // restore event ID from bundle 558 mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID); 559 mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 560 mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS); 561 mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS); 562 } 563 564 // start loading the data 565 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 566 null, null, null); 567 568 Button b = (Button) mView.findViewById(R.id.delete); 569 b.setOnClickListener(new OnClickListener() { 570 @Override 571 public void onClick(View v) { 572 if (!mCanModifyCalendar) { 573 return; 574 } 575 DeleteEventHelper deleteHelper = new DeleteEventHelper( 576 getActivity(), getActivity(), 577 !mIsDialog && !mIsTabletConfig /* exitWhenDone */); 578 deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); 579 }}); 580 581 // Hide Edit/Delete buttons if in full screen mode on a phone 582 if (savedInstanceState != null) { 583 mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false); 584 } 585 if (!mIsDialog && !mIsTabletConfig) { 586 mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE); 587 } 588 589 // Create a listener for the add reminder button 590 591 ImageButton reminderAddButton = (ImageButton) mView.findViewById(R.id.reminder_add); 592 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { 593 @Override 594 public void onClick(View v) { 595 addReminder(); 596 } 597 }; 598 reminderAddButton.setOnClickListener(addReminderOnClickListener); 599 600 // Set reminders variables 601 602 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity); 603 String defaultReminderString = prefs.getString( 604 GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING); 605 mDefaultReminderMinutes = Integer.parseInt(defaultReminderString); 606 prepareReminders(); 607 608 return mView; 609 } 610 611 private Runnable onDeleteRunnable = new Runnable() { 612 @Override 613 public void run() { 614 if (EventInfoFragment.this.mIsPaused) { 615 mDismissOnResume = true; 616 return; 617 } 618 if (EventInfoFragment.this.isVisible()) { 619 EventInfoFragment.this.dismiss(); 620 } 621 } 622 }; 623 624 // Sets the description: 625 // Set the expand/collapse button 626 // Expand/collapse the description according the the current status 627 private void updateDescription() { 628 // Description is short, hide button 629 if (mWhat.getLineCount() <= mDescLineNum) { 630 mDescButton.setVisibility(View.GONE); 631 return; 632 } 633 // Show button and set label according to the expand/collapse status 634 mDescButton.setVisibility(View.VISIBLE); 635 if (mShowMaxDescription) { 636 mDescButton.setText(mLessLabel); 637 mWhat.setLines(mWhat.getLineCount()); 638 } else { 639 mDescButton.setText(mMoreLabel); 640 mWhat.setLines(mDescLineNum); 641 } 642 } 643 644 private void updateTitle() { 645 Resources res = getActivity().getResources(); 646 if (mCanModifyCalendar && !mIsOrganizer) { 647 getActivity().setTitle(res.getString(R.string.event_info_title_invite)); 648 } else { 649 getActivity().setTitle(res.getString(R.string.event_info_title)); 650 } 651 } 652 653 /** 654 * Initializes the event cursor, which is expected to point to the first 655 * (and only) result from a query. 656 * @return true if the cursor is empty. 657 */ 658 private boolean initEventCursor() { 659 if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) { 660 return true; 661 } 662 mEventCursor.moveToFirst(); 663 mEventId = mEventCursor.getInt(EVENT_INDEX_ID); 664 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 665 mIsRepeating = !TextUtils.isEmpty(rRule); 666 mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)?true:false; 667 mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS); 668 mCalendarAllowedReminders = mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS); 669 return false; 670 } 671 672 @SuppressWarnings("fallthrough") 673 private void initAttendeesCursor(View view) { 674 mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE; 675 mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 676 mNumOfAttendees = 0; 677 if (mAttendeesCursor != null) { 678 mNumOfAttendees = mAttendeesCursor.getCount(); 679 if (mAttendeesCursor.moveToFirst()) { 680 mAcceptedAttendees.clear(); 681 mDeclinedAttendees.clear(); 682 mTentativeAttendees.clear(); 683 mNoResponseAttendees.clear(); 684 685 do { 686 int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 687 String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME); 688 String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL); 689 690 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE && 691 mCalendarOwnerAccount.equalsIgnoreCase(email)) { 692 mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID); 693 mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 694 } else { 695 // Don't show your own status in the list because: 696 // 1) it doesn't make sense for event without other guests. 697 // 2) there's a spinner for that for events with guests. 698 switch(status) { 699 case Attendees.ATTENDEE_STATUS_ACCEPTED: 700 mAcceptedAttendees.add(new Attendee(name, email, 701 Attendees.ATTENDEE_STATUS_ACCEPTED)); 702 break; 703 case Attendees.ATTENDEE_STATUS_DECLINED: 704 mDeclinedAttendees.add(new Attendee(name, email, 705 Attendees.ATTENDEE_STATUS_DECLINED)); 706 break; 707 case Attendees.ATTENDEE_STATUS_TENTATIVE: 708 mTentativeAttendees.add(new Attendee(name, email, 709 Attendees.ATTENDEE_STATUS_TENTATIVE)); 710 break; 711 default: 712 mNoResponseAttendees.add(new Attendee(name, email, 713 Attendees.ATTENDEE_STATUS_NONE)); 714 } 715 } 716 } while (mAttendeesCursor.moveToNext()); 717 mAttendeesCursor.moveToFirst(); 718 719 updateAttendees(view); 720 } 721 } 722 } 723 724 @Override 725 public void onSaveInstanceState(Bundle outState) { 726 super.onSaveInstanceState(outState); 727 outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId); 728 outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis); 729 outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis); 730 outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog); 731 outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, mAttendeeResponseFromIntent); 732 } 733 734 735 @Override 736 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 737 super.onCreateOptionsMenu(menu, inflater); 738 // Show edit/delete buttons only in non-dialog configuration on a phone 739 if (!mIsDialog && !mIsTabletConfig) { 740 inflater.inflate(R.menu.event_info_title_bar, menu); 741 mMenu = menu; 742 } 743 } 744 745 746 @Override 747 public void onDestroyView() { 748 if (saveResponse() || saveReminders()) { 749 Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show(); 750 } 751 super.onDestroyView(); 752 } 753 754 @Override 755 public void onDestroy() { 756 if (mEventCursor != null) { 757 mEventCursor.close(); 758 } 759 if (mCalendarsCursor != null) { 760 mCalendarsCursor.close(); 761 } 762 if (mAttendeesCursor != null) { 763 mAttendeesCursor.close(); 764 } 765 super.onDestroy(); 766 } 767 768 /** 769 * Asynchronously saves the response to an invitation if the user changed 770 * the response. Returns true if the database will be updated. 771 * 772 * @return true if the database will be changed 773 */ 774 private boolean saveResponse() { 775 if (mAttendeesCursor == null || mEventCursor == null) { 776 return false; 777 } 778 779 RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value); 780 int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId()); 781 if (status == Attendees.ATTENDEE_STATUS_NONE) { 782 return false; 783 } 784 785 // If the status has not changed, then don't update the database 786 if (status == mOriginalAttendeeResponse) { 787 return false; 788 } 789 790 // If we never got an owner attendee id we can't set the status 791 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) { 792 return false; 793 } 794 795 if (!mIsRepeating) { 796 // This is a non-repeating event 797 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 798 return true; 799 } 800 801 // This is a repeating event 802 int whichEvents = mEditResponseHelper.getWhichEvents(); 803 switch (whichEvents) { 804 case -1: 805 return false; 806 case UPDATE_SINGLE: 807 createExceptionResponse(mEventId, status); 808 return true; 809 case UPDATE_ALL: 810 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 811 return true; 812 default: 813 Log.e(TAG, "Unexpected choice for updating invitation response"); 814 break; 815 } 816 return false; 817 } 818 819 private void updateResponse(long eventId, long attendeeId, int status) { 820 // Update the attendee status in the attendees table. the provider 821 // takes care of updating the self attendance status. 822 ContentValues values = new ContentValues(); 823 824 if (!TextUtils.isEmpty(mCalendarOwnerAccount)) { 825 values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount); 826 } 827 values.put(Attendees.ATTENDEE_STATUS, status); 828 values.put(Attendees.EVENT_ID, eventId); 829 830 Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId); 831 832 mHandler.startUpdate(mHandler.getNextToken(), null, uri, values, 833 null, null, Utils.UNDO_DELAY); 834 } 835 836 /** 837 * Creates an exception to a recurring event. The only change we're making is to the 838 * "self attendee status" value. The provider will take care of updating the corresponding 839 * Attendees.attendeeStatus entry. 840 * 841 * @param eventId The recurring event. 842 * @param status The new value for selfAttendeeStatus. 843 */ 844 private void createExceptionResponse(long eventId, int status) { 845 ContentValues values = new ContentValues(); 846 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis); 847 values.put(Events.SELF_ATTENDEE_STATUS, status); 848 values.put(Events.STATUS, Events.STATUS_CONFIRMED); 849 850 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 851 Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, 852 String.valueOf(eventId)); 853 ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build()); 854 855 mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops, 856 Utils.UNDO_DELAY); 857 } 858 859 public static int getResponseFromButtonId(int buttonId) { 860 int response; 861 switch (buttonId) { 862 case R.id.response_yes: 863 response = Attendees.ATTENDEE_STATUS_ACCEPTED; 864 break; 865 case R.id.response_maybe: 866 response = Attendees.ATTENDEE_STATUS_TENTATIVE; 867 break; 868 case R.id.response_no: 869 response = Attendees.ATTENDEE_STATUS_DECLINED; 870 break; 871 default: 872 response = Attendees.ATTENDEE_STATUS_NONE; 873 } 874 return response; 875 } 876 877 public static int findButtonIdForResponse(int response) { 878 int buttonId; 879 switch (response) { 880 case Attendees.ATTENDEE_STATUS_ACCEPTED: 881 buttonId = R.id.response_yes; 882 break; 883 case Attendees.ATTENDEE_STATUS_TENTATIVE: 884 buttonId = R.id.response_maybe; 885 break; 886 case Attendees.ATTENDEE_STATUS_DECLINED: 887 buttonId = R.id.response_no; 888 break; 889 default: 890 buttonId = -1; 891 } 892 return buttonId; 893 } 894 895 private void doEdit() { 896 Context c = getActivity(); 897 // This ensures that we aren't in the process of closing and have been 898 // unattached already 899 if (c != null) { 900 CalendarController.getInstance(c).sendEventRelatedEvent( 901 this, EventType.VIEW_EVENT_DETAILS, mEventId, mStartMillis, mEndMillis, 0 902 , 0, -1); 903 } 904 } 905 906 private void updateEvent(View view) { 907 if (mEventCursor == null || view == null) { 908 return; 909 } 910 911 String eventName = mEventCursor.getString(EVENT_INDEX_TITLE); 912 if (eventName == null || eventName.length() == 0) { 913 eventName = getActivity().getString(R.string.no_title_label); 914 } 915 916 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 917 String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION); 918 String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION); 919 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 920 String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); 921 String organizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER); 922 923 mColor = Utils.getDisplayColorFromColor(mEventCursor.getInt(EVENT_INDEX_COLOR)); 924 mHeadlines.setBackgroundColor(mColor); 925 926 // What 927 if (eventName != null) { 928 setTextCommon(view, R.id.title, eventName); 929 } 930 931 // When 932 // Set the date and repeats (if any) 933 String whenDate; 934 int flagsTime = DateUtils.FORMAT_SHOW_TIME; 935 int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY | 936 DateUtils.FORMAT_SHOW_YEAR; 937 938 if (DateFormat.is24HourFormat(getActivity())) { 939 flagsTime |= DateUtils.FORMAT_24HOUR; 940 } 941 942 // Put repeat after the date (if any) 943 String repeatString = null; 944 if (!TextUtils.isEmpty(rRule)) { 945 EventRecurrence eventRecurrence = new EventRecurrence(); 946 eventRecurrence.parse(rRule); 947 Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater)); 948 if (allDay) { 949 date.timezone = Time.TIMEZONE_UTC; 950 } 951 date.set(mStartMillis); 952 eventRecurrence.setStartDate(date); 953 repeatString = EventRecurrenceFormatter.getRepeatString( 954 getActivity().getResources(), eventRecurrence); 955 } 956 // If an all day event , show the date without the time 957 if (allDay) { 958 Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault()); 959 whenDate = DateUtils.formatDateRange(getActivity(), f, mStartMillis, mStartMillis, 960 flagsDate, Time.TIMEZONE_UTC).toString(); 961 if (repeatString != null) { 962 setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")"); 963 } else { 964 setTextCommon(view, R.id.when_date, whenDate); 965 } 966 view.findViewById(R.id.when_time).setVisibility(View.GONE); 967 968 } else { 969 // Show date for none all-day events 970 whenDate = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flagsDate); 971 String whenTime = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, 972 flagsTime); 973 if (repeatString != null) { 974 setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")"); 975 } else { 976 setTextCommon(view, R.id.when_date, whenDate); 977 } 978 979 // Show the event timezone if it is different from the local timezone after the time 980 // TODO: Fix comparison of Timezone 981 String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater); 982 if (!TextUtils.equals(localTimezone, eventTimezone)) { 983 String displayName; 984 // Figure out if this is in DST 985 Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater)); 986 if (allDay) { 987 date.timezone = Time.TIMEZONE_UTC; 988 } 989 date.set(mStartMillis); 990 991 TimeZone tz = TimeZone.getTimeZone(localTimezone); 992 if (tz == null || tz.getID().equals("GMT")) { 993 displayName = localTimezone; 994 } else { 995 displayName = tz.getDisplayName(date.isDst != 0, TimeZone.LONG); 996 } 997 setTextCommon(view, R.id.when_time, whenTime + " (" + displayName + ")"); 998 } 999 else { 1000 setTextCommon(view, R.id.when_time, whenTime); 1001 } 1002 } 1003 1004 1005 // Organizer view is setup in the updateCalendar method 1006 1007 1008 // Where 1009 if (location == null || location.trim().length() == 0) { 1010 setVisibilityCommon(view, R.id.where, View.GONE); 1011 } else { 1012 final TextView textView = mWhere; 1013 if (textView != null) { 1014 textView.setAutoLinkMask(0); 1015 textView.setText(location.trim()); 1016 if (!Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES 1017 | Linkify.MAP_ADDRESSES)) { 1018 Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); 1019 } 1020 textView.setOnTouchListener(new OnTouchListener() { 1021 @Override 1022 public boolean onTouch(View v, MotionEvent event) { 1023 try { 1024 return v.onTouchEvent(event); 1025 } catch (ActivityNotFoundException e) { 1026 // ignore 1027 return true; 1028 } 1029 } 1030 }); 1031 } 1032 } 1033 1034 // Description 1035 if (description != null && description.length() != 0) { 1036 setTextCommon(view, R.id.description, description); 1037 } 1038 updateDescription (); // Expand or collapse full description 1039 } 1040 1041 private void sendAccessibilityEvent() { 1042 AccessibilityManager am = 1043 (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE); 1044 if (!am.isEnabled()) { 1045 return; 1046 } 1047 1048 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 1049 event.setClassName(getClass().getName()); 1050 event.setPackageName(getActivity().getPackageName()); 1051 List<CharSequence> text = event.getText(); 1052 1053 addFieldToAccessibilityEvent(text, mTitle); 1054 addFieldToAccessibilityEvent(text, mWhen); 1055 addFieldToAccessibilityEvent(text, mWhere); 1056 addFieldToAccessibilityEvent(text, mWhat); 1057 addFieldToAccessibilityEvent(text, mAttendees); 1058 1059 RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value); 1060 if (response.getVisibility() == View.VISIBLE) { 1061 int id = response.getCheckedRadioButtonId(); 1062 if (id != View.NO_ID) { 1063 text.add(((TextView) getView().findViewById(R.id.response_label)).getText()); 1064 text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE)); 1065 } 1066 } 1067 1068 am.sendAccessibilityEvent(event); 1069 } 1070 1071 /** 1072 * @param text 1073 */ 1074 private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView view) { 1075 if (view == null) { 1076 return; 1077 } 1078 String str = view.toString().trim(); 1079 if (!TextUtils.isEmpty(str)) { 1080 text.add(mTitle.getText()); 1081 text.add(PERIOD_SPACE); 1082 } 1083 } 1084 1085 private void updateCalendar(View view) { 1086 mCalendarOwnerAccount = ""; 1087 if (mCalendarsCursor != null && mEventCursor != null) { 1088 mCalendarsCursor.moveToFirst(); 1089 String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 1090 mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount; 1091 mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0; 1092 1093 String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 1094 1095 // start duplicate calendars query 1096 mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI, 1097 CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE, 1098 new String[] {displayName}, null); 1099 1100 String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER); 1101 mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer); 1102 setTextCommon(view, R.id.organizer, eventOrganizer); 1103 if (!mIsOrganizer) { 1104 setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); 1105 } else { 1106 setVisibilityCommon(view, R.id.organizer_container, View.GONE); 1107 } 1108 mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0; 1109 mCanModifyCalendar = 1110 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CAL_ACCESS_CONTRIBUTOR; 1111 mIsBusyFreeCalendar = 1112 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY; 1113 1114 if (!mIsBusyFreeCalendar) { 1115 Button b = (Button) mView.findViewById(R.id.edit); 1116 b.setEnabled(true); 1117 b.setOnClickListener(new OnClickListener() { 1118 @Override 1119 public void onClick(View v) { 1120 doEdit(); 1121 // For dialogs, just close the fragment 1122 // For full screen, close activity on phone, leave it for tablet 1123 if (mIsDialog) { 1124 EventInfoFragment.this.dismiss(); 1125 } 1126 else if (!mIsTabletConfig){ 1127 getActivity().finish(); 1128 } 1129 } 1130 }); 1131 } 1132 if (!mCanModifyCalendar) { 1133 if (mIsDialog) { 1134 mView.findViewById(R.id.delete).setEnabled(false); 1135 } 1136 else { 1137 MenuItem item = mMenu.findItem(R.id.info_action_delete); 1138 if (item != null) { 1139 item.setVisible(false); 1140 } 1141 } 1142 } 1143 } else { 1144 setVisibilityCommon(view, R.id.calendar, View.GONE); 1145 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); 1146 } 1147 } 1148 1149 private void updateAttendees(View view) { 1150 1151 if (mAcceptedAttendees.size() + mDeclinedAttendees.size() + 1152 mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) { 1153 (mLongAttendees).addAttendees(mAcceptedAttendees); 1154 (mLongAttendees).addAttendees(mDeclinedAttendees); 1155 (mLongAttendees).addAttendees(mTentativeAttendees); 1156 (mLongAttendees).addAttendees(mNoResponseAttendees); 1157 mLongAttendees.setEnabled(false); 1158 mLongAttendees.setVisibility(View.VISIBLE); 1159 } else { 1160 mLongAttendees.setVisibility(View.GONE); 1161 } 1162 } 1163 1164 public void initReminders(View view, Cursor cursor) { 1165 1166 // Add reminders 1167 while (cursor.moveToNext()) { 1168 int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES); 1169 int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD); 1170 mOriginalReminders.add(ReminderEntry.valueOf(minutes, method)); 1171 } 1172 // Sort appropriately for display (by time, then type) 1173 Collections.sort(mOriginalReminders); 1174 1175 // Load the labels and corresponding numeric values for the minutes and methods lists 1176 // from the assets. If we're switching calendars, we need to clear and re-populate the 1177 // lists (which may have elements added and removed based on calendar properties). This 1178 // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a 1179 // new event that aren't in the default set. 1180 Resources r = mActivity.getResources(); 1181 mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values); 1182 mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels); 1183 mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values); 1184 mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels); 1185 1186 // Remove any reminder methods that aren't allowed for this calendar. If this is 1187 // a new event, mCalendarAllowedReminders may not be set the first time we're called. 1188 if (mCalendarAllowedReminders != null) { 1189 EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels, 1190 mCalendarAllowedReminders); 1191 } 1192 1193 int numReminders = 0; 1194 if (mHasAlarm) { 1195 ArrayList<ReminderEntry> reminders = mOriginalReminders; 1196 numReminders = reminders.size(); 1197 // Insert any minute values that aren't represented in the minutes list. 1198 for (ReminderEntry re : reminders) { 1199 EventViewUtils.addMinutesToList( 1200 mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes()); 1201 } 1202 // Create a UI element for each reminder. We display all of the reminders we get 1203 // from the provider, even if the count exceeds the calendar maximum. (Also, for 1204 // a new event, we won't have a maxReminders value available.) 1205 for (ReminderEntry re : reminders) { 1206 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1207 mReminderMinuteValues, mReminderMinuteLabels, 1208 mReminderMethodValues, mReminderMethodLabels, 1209 re, Integer.MAX_VALUE); 1210 } 1211 } 1212 } 1213 1214 private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) { 1215 if (attendees.size() <= 0) { 1216 return; 1217 } 1218 1219 int begin = sb.length(); 1220 boolean firstTime = sb.length() == 0; 1221 1222 if (firstTime == false) { 1223 begin += 2; // skip over the ", " for formatting. 1224 } 1225 1226 for (Attendee attendee : attendees) { 1227 if (firstTime) { 1228 firstTime = false; 1229 } else { 1230 sb.append(", "); 1231 } 1232 1233 String name = attendee.getDisplayName(); 1234 sb.append(name); 1235 } 1236 1237 switch (type) { 1238 case Attendees.ATTENDEE_STATUS_ACCEPTED: 1239 break; 1240 case Attendees.ATTENDEE_STATUS_DECLINED: 1241 sb.setSpan(new StrikethroughSpan(), begin, sb.length(), 1242 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1243 // fall through 1244 default: 1245 // The last INCLUSIVE causes the foreground color to be applied 1246 // to the rest of the span. If not, the comma at the end of the 1247 // declined or tentative may be black. 1248 sb.setSpan(new ForegroundColorSpan(0xFF999999), begin, sb.length(), 1249 Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 1250 break; 1251 } 1252 } 1253 1254 void updateResponse(View view) { 1255 // we only let the user accept/reject/etc. a meeting if: 1256 // a) you can edit the event's containing calendar AND 1257 // b) you're not the organizer and only attendee AND 1258 // c) organizerCanRespond is enabled for the calendar 1259 // (if the attendee data has been hidden, the visible number of attendees 1260 // will be 1 -- the calendar owner's). 1261 // (there are more cases involved to be 100% accurate, such as 1262 // paying attention to whether or not an attendee status was 1263 // included in the feed, but we're currently omitting those corner cases 1264 // for simplicity). 1265 1266 // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel. 1267 if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) || 1268 (mIsOrganizer && !mOwnerCanRespond)) { 1269 setVisibilityCommon(view, R.id.response_container, View.GONE); 1270 return; 1271 } 1272 1273 setVisibilityCommon(view, R.id.response_container, View.VISIBLE); 1274 1275 1276 int response; 1277 if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) { 1278 response = mAttendeeResponseFromIntent; 1279 } else { 1280 response = mOriginalAttendeeResponse; 1281 } 1282 1283 int buttonToCheck = findButtonIdForResponse(response); 1284 RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value); 1285 radioGroup.check(buttonToCheck); // -1 clear all radio buttons 1286 radioGroup.setOnCheckedChangeListener(this); 1287 } 1288 1289 private void setTextCommon(View view, int id, CharSequence text) { 1290 TextView textView = (TextView) view.findViewById(id); 1291 if (textView == null) 1292 return; 1293 textView.setText(text); 1294 } 1295 1296 private void setVisibilityCommon(View view, int id, int visibility) { 1297 View v = view.findViewById(id); 1298 if (v != null) { 1299 v.setVisibility(visibility); 1300 } 1301 return; 1302 } 1303 1304 /** 1305 * Taken from com.google.android.gm.HtmlConversationActivity 1306 * 1307 * Send the intent that shows the Contact info corresponding to the email address. 1308 */ 1309 public void showContactInfo(Attendee attendee, Rect rect) { 1310 // First perform lookup query to find existing contact 1311 final ContentResolver resolver = getActivity().getContentResolver(); 1312 final String address = attendee.mEmail; 1313 final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI, 1314 Uri.encode(address)); 1315 final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri); 1316 1317 if (lookupUri != null) { 1318 // Found matching contact, trigger QuickContact 1319 QuickContact.showQuickContact(getActivity(), rect, lookupUri, 1320 QuickContact.MODE_MEDIUM, null); 1321 } else { 1322 // No matching contact, ask user to create one 1323 final Uri mailUri = Uri.fromParts("mailto", address, null); 1324 final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri); 1325 1326 // Pass along full E-mail string for possible create dialog 1327 Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null); 1328 intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString()); 1329 1330 // Only provide personal name hint if we have one 1331 final String senderPersonal = attendee.mName; 1332 if (!TextUtils.isEmpty(senderPersonal)) { 1333 intent.putExtra(Intents.Insert.NAME, senderPersonal); 1334 } 1335 1336 startActivity(intent); 1337 } 1338 } 1339 1340 @Override 1341 public void onPause() { 1342 mIsPaused = true; 1343 mHandler.removeCallbacks(onDeleteRunnable); 1344 super.onPause(); 1345 } 1346 1347 @Override 1348 public void onResume() { 1349 super.onResume(); 1350 mIsPaused = false; 1351 if (mDismissOnResume) { 1352 mHandler.post(onDeleteRunnable); 1353 } 1354 } 1355 1356 @Override 1357 public void eventsChanged() { 1358 } 1359 1360 @Override 1361 public long getSupportedEventTypes() { 1362 return EventType.EVENTS_CHANGED; 1363 } 1364 1365 @Override 1366 public void handleEvent(EventInfo event) { 1367 if (event.eventType == EventType.EVENTS_CHANGED) { 1368 // reload the data 1369 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 1370 null, null, null); 1371 } 1372 1373 } 1374 1375 1376 @Override 1377 public void onClick(View view) { 1378 1379 // This must be a click on one of the "remove reminder" buttons 1380 LinearLayout reminderItem = (LinearLayout) view.getParent(); 1381 LinearLayout parent = (LinearLayout) reminderItem.getParent(); 1382 parent.removeView(reminderItem); 1383 mReminderViews.remove(reminderItem); 1384 } 1385 1386 1387 /** 1388 * Add a new reminder when the user hits the "add reminder" button. We use the default 1389 * reminder time and method. 1390 */ 1391 private void addReminder() { 1392 // TODO: when adding a new reminder, make it different from the 1393 // last one in the list (if any). 1394 if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) { 1395 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1396 mReminderMinuteValues, mReminderMinuteLabels, 1397 mReminderMethodValues, mReminderMethodLabels, 1398 ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), 1399 mMaxReminders); 1400 } else { 1401 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1402 mReminderMinuteValues, mReminderMinuteLabels, 1403 mReminderMethodValues, mReminderMethodLabels, 1404 ReminderEntry.valueOf(mDefaultReminderMinutes), 1405 mMaxReminders); 1406 } 1407 } 1408 1409 1410 private void prepareReminders() { 1411 Resources r = mActivity.getResources(); 1412 mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values); 1413 mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels); 1414 mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values); 1415 mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels); 1416 } 1417 1418 1419 private boolean saveReminders() { 1420 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3); 1421 1422 // Read reminders from UI 1423 mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews, 1424 mReminderMinuteValues, mReminderMethodValues); 1425 1426 // Check if there are any changes in the reminder 1427 boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders, 1428 mOriginalReminders, false /* no force save */); 1429 1430 if (!changed) { 1431 return false; 1432 } 1433 1434 // save new reminders 1435 AsyncQueryService service = new AsyncQueryService(getActivity()); 1436 service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0); 1437 // Update the "hasAlarm" field for the event 1438 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 1439 int len = mReminders.size(); 1440 boolean hasAlarm = len > 0; 1441 if (hasAlarm != mHasAlarm) { 1442 ContentValues values = new ContentValues(); 1443 values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0); 1444 service.startUpdate(0, null, uri, values, null, null, 0); 1445 } 1446 return true; 1447 } 1448 1449 /** 1450 * Loads an integer array asset into a list. 1451 */ 1452 private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) { 1453 int[] vals = r.getIntArray(resNum); 1454 int size = vals.length; 1455 ArrayList<Integer> list = new ArrayList<Integer>(size); 1456 1457 for (int i = 0; i < size; i++) { 1458 list.add(vals[i]); 1459 } 1460 1461 return list; 1462 } 1463 /** 1464 * Loads a String array asset into a list. 1465 */ 1466 private static ArrayList<String> loadStringArray(Resources r, int resNum) { 1467 String[] labels = r.getStringArray(resNum); 1468 ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels)); 1469 return list; 1470 } 1471 1472} 1473