EventInfoFragment.java revision 7c6236d5553dc9f3d004ebbed794249713a11d19
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.event.EditEventHelper; 22 23import android.app.Activity; 24import android.app.Dialog; 25import android.app.DialogFragment; 26import android.content.ActivityNotFoundException; 27import android.content.ContentProviderOperation; 28import android.content.ContentResolver; 29import android.content.ContentUris; 30import android.content.ContentValues; 31import android.content.Context; 32import android.content.Intent; 33import android.content.res.Resources; 34import android.database.Cursor; 35import android.graphics.Rect; 36import android.graphics.Typeface; 37import android.net.Uri; 38import android.os.Bundle; 39import android.pim.EventRecurrence; 40import android.provider.Calendar; 41import android.provider.Calendar.Attendees; 42import android.provider.Calendar.Calendars; 43import android.provider.Calendar.Events; 44import android.provider.ContactsContract; 45import android.provider.ContactsContract.CommonDataKinds; 46import android.provider.ContactsContract.Intents; 47import android.provider.ContactsContract.QuickContact; 48import android.text.Spannable; 49import android.text.SpannableStringBuilder; 50import android.text.TextUtils; 51import android.text.format.DateFormat; 52import android.text.format.DateUtils; 53import android.text.format.Time; 54import android.text.style.ForegroundColorSpan; 55import android.text.style.StrikethroughSpan; 56import android.text.style.StyleSpan; 57import android.text.util.Linkify; 58import android.text.util.Rfc822Token; 59import android.util.Log; 60import android.view.Gravity; 61import android.view.LayoutInflater; 62import android.view.MotionEvent; 63import android.view.View; 64import android.view.View.OnClickListener; 65import android.view.View.OnTouchListener; 66import android.view.ViewGroup; 67import android.view.Window; 68import android.view.WindowManager; 69import android.view.accessibility.AccessibilityEvent; 70import android.view.accessibility.AccessibilityManager; 71import android.widget.AdapterView; 72import android.widget.Button; 73import android.widget.RadioButton; 74import android.widget.RadioGroup; 75import android.widget.RadioGroup.OnCheckedChangeListener; 76import android.widget.TextView; 77import android.widget.Toast; 78 79import java.util.ArrayList; 80import java.util.List; 81import java.util.regex.Pattern; 82 83public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener, 84 CalendarController.EventHandler { 85 public static final boolean DEBUG = false; 86 87 public static final String TAG = "EventInfoFragment"; 88 89 private static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; 90 private static final String BUNDLE_KEY_START_MILLIS = "key_start_millis"; 91 private static final String BUNDLE_KEY_END_MILLIS = "key_end_millis"; 92 private static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog"; 93 94 private static final String PERIOD_SPACE = ". "; 95 96 /** 97 * These are the corresponding indices into the array of strings 98 * "R.array.change_response_labels" in the resource file. 99 */ 100 static final int UPDATE_SINGLE = 0; 101 static final int UPDATE_ALL = 1; 102 103 // Query tokens for QueryHandler 104 private static final int TOKEN_QUERY_EVENT = 1 << 0; 105 private static final int TOKEN_QUERY_CALENDARS = 1 << 1; 106 private static final int TOKEN_QUERY_ATTENDEES = 1 << 2; 107 private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3; 108 private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS 109 | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT; 110 private int mCurrentQuery = 0; 111 112 private static final String[] EVENT_PROJECTION = new String[] { 113 Events._ID, // 0 do not remove; used in DeleteEventHelper 114 Events.TITLE, // 1 do not remove; used in DeleteEventHelper 115 Events.RRULE, // 2 do not remove; used in DeleteEventHelper 116 Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper 117 Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper 118 Events.DTSTART, // 5 do not remove; used in DeleteEventHelper 119 Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper 120 Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper 121 Events.DESCRIPTION, // 8 122 Events.EVENT_LOCATION, // 9 123 Calendars.ACCESS_LEVEL, // 10 124 Calendars.COLOR, // 11 125 Events.HAS_ATTENDEE_DATA, // 12 126 Events.ORGANIZER, // 13 127 Events.ORIGINAL_EVENT // 14 do not remove; used in DeleteEventHelper 128 }; 129 private static final int EVENT_INDEX_ID = 0; 130 private static final int EVENT_INDEX_TITLE = 1; 131 private static final int EVENT_INDEX_RRULE = 2; 132 private static final int EVENT_INDEX_ALL_DAY = 3; 133 private static final int EVENT_INDEX_CALENDAR_ID = 4; 134 private static final int EVENT_INDEX_SYNC_ID = 6; 135 private static final int EVENT_INDEX_EVENT_TIMEZONE = 7; 136 private static final int EVENT_INDEX_DESCRIPTION = 8; 137 private static final int EVENT_INDEX_EVENT_LOCATION = 9; 138 private static final int EVENT_INDEX_ACCESS_LEVEL = 10; 139 private static final int EVENT_INDEX_COLOR = 11; 140 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12; 141 private static final int EVENT_INDEX_ORGANIZER = 13; 142 143 private static final String[] ATTENDEES_PROJECTION = new String[] { 144 Attendees._ID, // 0 145 Attendees.ATTENDEE_NAME, // 1 146 Attendees.ATTENDEE_EMAIL, // 2 147 Attendees.ATTENDEE_RELATIONSHIP, // 3 148 Attendees.ATTENDEE_STATUS, // 4 149 }; 150 private static final int ATTENDEES_INDEX_ID = 0; 151 private static final int ATTENDEES_INDEX_NAME = 1; 152 private static final int ATTENDEES_INDEX_EMAIL = 2; 153 private static final int ATTENDEES_INDEX_RELATIONSHIP = 3; 154 private static final int ATTENDEES_INDEX_STATUS = 4; 155 156 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; 157 158 private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, " 159 + Attendees.ATTENDEE_EMAIL + " ASC"; 160 161 static final String[] CALENDARS_PROJECTION = new String[] { 162 Calendars._ID, // 0 163 Calendars.DISPLAY_NAME, // 1 164 Calendars.OWNER_ACCOUNT, // 2 165 Calendars.ORGANIZER_CAN_RESPOND // 3 166 }; 167 static final int CALENDARS_INDEX_DISPLAY_NAME = 1; 168 static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 169 static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3; 170 171 static final String CALENDARS_WHERE = Calendars._ID + "=?"; 172 static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.DISPLAY_NAME + "=?"; 173 174 private View mView; 175 176 private Uri mUri; 177 private long mEventId; 178 private Cursor mEventCursor; 179 private Cursor mAttendeesCursor; 180 private Cursor mCalendarsCursor; 181 private static float mScale = 0; // Used for supporting different screen densities 182 183 private long mStartMillis; 184 private long mEndMillis; 185 186 private boolean mHasAttendeeData; 187 private boolean mIsOrganizer; 188 private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 189 private boolean mOwnerCanRespond; 190 private String mCalendarOwnerAccount; 191 private boolean mCanModifyCalendar; 192 private boolean mIsBusyFreeCalendar; 193 private int mNumOfAttendees; 194 195 private EditResponseHelper mEditResponseHelper; 196 197 private int mOriginalAttendeeResponse; 198 private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE; 199 private boolean mIsRepeating; 200 201 private TextView mTitle; 202 private TextView mWhen; 203 private TextView mWhere; 204 private TextView mWhat; 205 private TextView mAttendees; 206 private TextView mCalendar; 207 208 private Pattern mWildcardPattern = Pattern.compile("^.*$"); 209 210 ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>(); 211 ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>(); 212 ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>(); 213 ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>(); 214 private int mColor; 215 216 private QueryHandler mHandler; 217 218 private Runnable mTZUpdater = new Runnable() { 219 @Override 220 public void run() { 221 updateEvent(mView); 222 } 223 }; 224 225 private static int DIALOG_WIDTH = 500; 226 private static int DIALOG_HEIGHT = 600; 227 private boolean mIsDialog = false; 228 private boolean mIsPaused = true; 229 private boolean mDismissOnResume = false; 230 private int mX = -1; 231 private int mY = -1; 232 233 private class QueryHandler extends AsyncQueryService { 234 public QueryHandler(Context context) { 235 super(context); 236 } 237 238 @Override 239 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 240 // if the activity is finishing, then close the cursor and return 241 final Activity activity = getActivity(); 242 if (activity == null || activity.isFinishing()) { 243 cursor.close(); 244 return; 245 } 246 247 switch (token) { 248 case TOKEN_QUERY_EVENT: 249 mEventCursor = Utils.matrixCursorFromCursor(cursor); 250 if (initEventCursor()) { 251 // The cursor is empty. This can happen if the event was 252 // deleted. 253 // FRAG_TODO we should no longer rely on Activity.finish() 254 activity.finish(); 255 return; 256 } 257 updateEvent(mView); 258 259 // start calendar query 260 Uri uri = Calendars.CONTENT_URI; 261 String[] args = new String[] { 262 Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))}; 263 startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION, 264 CALENDARS_WHERE, args, null); 265 break; 266 case TOKEN_QUERY_CALENDARS: 267 mCalendarsCursor = Utils.matrixCursorFromCursor(cursor); 268 updateCalendar(mView); 269 // FRAG_TODO fragments shouldn't set the title anymore 270 updateTitle(); 271 // update the action bar since our option set might have changed 272 activity.invalidateOptionsMenu(); 273 274 if (!mIsBusyFreeCalendar) { 275 args = new String[] { Long.toString(mEventId) }; 276 277 // start attendees query 278 uri = Attendees.CONTENT_URI; 279 startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION, 280 ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER); 281 } else { 282 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES); 283 } 284 break; 285 case TOKEN_QUERY_ATTENDEES: 286 mAttendeesCursor = Utils.matrixCursorFromCursor(cursor); 287 initAttendeesCursor(mView); 288 updateResponse(mView); 289 break; 290 case TOKEN_QUERY_DUPLICATE_CALENDARS: 291 Resources res = activity.getResources(); 292 SpannableStringBuilder sb = new SpannableStringBuilder(); 293 294 // Label 295 String label = res.getString(R.string.view_event_calendar_label); 296 sb.append(label).append(" "); 297 sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(), 298 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 299 300 // Calendar display name 301 String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 302 sb.append(calendarName); 303 304 // Show email account if display name is not unique and 305 // display name != email 306 String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 307 if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) { 308 sb.append(" (").append(email).append(")"); 309 } 310 311 mCalendar.setText(sb); 312 break; 313 } 314 cursor.close(); 315 sendAccessibilityEventIfQueryDone(token); 316 } 317 318 } 319 320 private void sendAccessibilityEventIfQueryDone(int token) { 321 mCurrentQuery |= token; 322 if (mCurrentQuery == TOKEN_QUERY_ALL) { 323 sendAccessibilityEvent(); 324 } 325 } 326 327 public EventInfoFragment() { 328 mUri = null; 329 } 330 331 public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, 332 int attendeeResponse) { 333 if (mScale == 0) { 334 mScale = context.getResources().getDisplayMetrics().density; 335 if (mScale != 1) { 336 DIALOG_WIDTH *= mScale; 337 DIALOG_HEIGHT *= mScale; 338 } 339 } 340 341 setStyle(DialogFragment.STYLE_NO_TITLE, 0); 342 mUri = uri; 343 mStartMillis = startMillis; 344 mEndMillis = endMillis; 345 mAttendeeResponseFromIntent = attendeeResponse; 346 } 347 348 public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, 349 int attendeeResponse) { 350 this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis, 351 endMillis, attendeeResponse); 352 mEventId = eventId; 353 } 354 355 @Override 356 public void onActivityCreated(Bundle savedInstanceState) { 357 super.onActivityCreated(savedInstanceState); 358 359 if (savedInstanceState != null) { 360 mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false); 361 } 362 363 if (mIsDialog) { 364 applyDialogParams(); 365 } 366 } 367 368 private void applyDialogParams() { 369 Dialog dialog = getDialog(); 370 dialog.setCanceledOnTouchOutside(true); 371 372 Window window = dialog.getWindow(); 373 window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 374 375 WindowManager.LayoutParams a = window.getAttributes(); 376 a.dimAmount = .4f; 377 378 a.width = DIALOG_WIDTH; 379 a.height = DIALOG_HEIGHT; 380 381 if (mX != -1 || mY != -1) { 382 a.x = mX - a.width - 64; 383 if (a.x < 0) { 384 a.x = mX + 64; 385 } 386 a.y = mY - 64; 387 a.gravity = Gravity.LEFT | Gravity.TOP; 388 } 389 390 window.setAttributes(a); 391 } 392 393 public void setDialogParams(int x, int y) { 394 mIsDialog = true; 395 mX = x; 396 mY = y; 397 } 398 399 // Implements OnCheckedChangeListener 400 @Override 401 public void onCheckedChanged(RadioGroup group, int checkedId) { 402 // If this is not a repeating event, then don't display the dialog 403 // asking which events to change. 404 if (!mIsRepeating) { 405 return; 406 } 407 408 // If the selection is the same as the original, then don't display the 409 // dialog asking which events to change. 410 if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) { 411 return; 412 } 413 414 // This is a repeating event. We need to ask the user if they mean to 415 // change just this one instance or all instances. 416 mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents()); 417 } 418 419 public void onNothingSelected(AdapterView<?> parent) { 420 } 421 422 @Override 423 public void onAttach(Activity activity) { 424 super.onAttach(activity); 425 mEditResponseHelper = new EditResponseHelper(activity); 426 mHandler = new QueryHandler(activity); 427 } 428 429 @Override 430 public View onCreateView(LayoutInflater inflater, ViewGroup container, 431 Bundle savedInstanceState) { 432 mView = inflater.inflate(R.layout.event_info, container, false); 433 mTitle = (TextView) mView.findViewById(R.id.title); 434 mWhen = (TextView) mView.findViewById(R.id.when); 435 mWhere = (TextView) mView.findViewById(R.id.where); 436 mWhat = (TextView) mView.findViewById(R.id.description); 437 mAttendees = (TextView) mView.findViewById(R.id.attendee_list); 438 mCalendar = (TextView) mView.findViewById(R.id.calendar); 439 440 if (mUri == null) { 441 // restore event ID from bundle 442 mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID); 443 mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 444 mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS); 445 mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS); 446 } 447 448 // start loading the data 449 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 450 null, null, null); 451 452 Button b = (Button) mView.findViewById(R.id.delete); 453 b.setOnClickListener(new OnClickListener() { 454 @Override 455 public void onClick(View v) { 456 if (!mCanModifyCalendar) { 457 return; 458 } 459 DeleteEventHelper deleteHelper = new DeleteEventHelper( 460 getActivity(), getActivity(), false /* exitWhenDone */); 461 deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); 462 }}); 463 464 return mView; 465 } 466 467 private Runnable onDeleteRunnable = new Runnable() { 468 @Override 469 public void run() { 470 if (EventInfoFragment.this.mIsPaused) { 471 mDismissOnResume = true; 472 return; 473 } 474 if (EventInfoFragment.this.isVisible()) { 475 EventInfoFragment.this.dismiss(); 476 } 477 } 478 }; 479 480 private void updateTitle() { 481 Resources res = getActivity().getResources(); 482 if (mCanModifyCalendar && !mIsOrganizer) { 483 getActivity().setTitle(res.getString(R.string.event_info_title_invite)); 484 } else { 485 getActivity().setTitle(res.getString(R.string.event_info_title)); 486 } 487 } 488 489 /** 490 * Initializes the event cursor, which is expected to point to the first 491 * (and only) result from a query. 492 * @return true if the cursor is empty. 493 */ 494 private boolean initEventCursor() { 495 if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) { 496 return true; 497 } 498 mEventCursor.moveToFirst(); 499 mEventId = mEventCursor.getInt(EVENT_INDEX_ID); 500 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 501 mIsRepeating = !TextUtils.isEmpty(rRule); 502 return false; 503 } 504 505 private static class Attendee { 506 String mName; 507 String mEmail; 508 509 Attendee(String name, String email) { 510 mName = name; 511 mEmail = email; 512 } 513 514 String getDisplayName() { 515 if (TextUtils.isEmpty(mName)) { 516 return mEmail; 517 } else { 518 return mName; 519 } 520 } 521 } 522 523 @SuppressWarnings("fallthrough") 524 private void initAttendeesCursor(View view) { 525 mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE; 526 mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 527 mNumOfAttendees = 0; 528 if (mAttendeesCursor != null) { 529 mNumOfAttendees = mAttendeesCursor.getCount(); 530 if (mAttendeesCursor.moveToFirst()) { 531 mAcceptedAttendees.clear(); 532 mDeclinedAttendees.clear(); 533 mTentativeAttendees.clear(); 534 mNoResponseAttendees.clear(); 535 536 do { 537 int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 538 String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME); 539 String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL); 540 541 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE && 542 mCalendarOwnerAccount.equalsIgnoreCase(email)) { 543 mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID); 544 mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 545 } else { 546 // Don't show your own status in the list because: 547 // 1) it doesn't make sense for event without other guests. 548 // 2) there's a spinner for that for events with guests. 549 switch(status) { 550 case Attendees.ATTENDEE_STATUS_ACCEPTED: 551 mAcceptedAttendees.add(new Attendee(name, email)); 552 break; 553 case Attendees.ATTENDEE_STATUS_DECLINED: 554 mDeclinedAttendees.add(new Attendee(name, email)); 555 break; 556 case Attendees.ATTENDEE_STATUS_TENTATIVE: 557 mTentativeAttendees.add(new Attendee(name, email)); 558 break; 559 default: 560 mNoResponseAttendees.add(new Attendee(name, email)); 561 } 562 } 563 } while (mAttendeesCursor.moveToNext()); 564 mAttendeesCursor.moveToFirst(); 565 566 updateAttendees(view); 567 } 568 } 569 } 570 571 @Override 572 public void onSaveInstanceState(Bundle outState) { 573 super.onSaveInstanceState(outState); 574 outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId); 575 outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis); 576 outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis); 577 578 outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog); 579 } 580 581 582 @Override 583 public void onDestroyView() { 584 if (saveResponse()) { 585 Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show(); 586 } 587 super.onDestroyView(); 588 } 589 590 @Override 591 public void onDestroy() { 592 if (mEventCursor != null) { 593 mEventCursor.close(); 594 } 595 if (mCalendarsCursor != null) { 596 mCalendarsCursor.close(); 597 } 598 if (mAttendeesCursor != null) { 599 mAttendeesCursor.close(); 600 } 601 super.onDestroy(); 602 } 603 604 /** 605 * Asynchronously saves the response to an invitation if the user changed 606 * the response. Returns true if the database will be updated. 607 * 608 * @param cr the ContentResolver 609 * @return true if the database will be changed 610 */ 611 private boolean saveResponse() { 612 if (mAttendeesCursor == null || mEventCursor == null) { 613 return false; 614 } 615 616 RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value); 617 int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId()); 618 if (status == Attendees.ATTENDEE_STATUS_NONE) { 619 return false; 620 } 621 622 // If the status has not changed, then don't update the database 623 if (status == mOriginalAttendeeResponse) { 624 return false; 625 } 626 627 // If we never got an owner attendee id we can't set the status 628 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) { 629 return false; 630 } 631 632 if (!mIsRepeating) { 633 // This is a non-repeating event 634 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 635 return true; 636 } 637 638 // This is a repeating event 639 int whichEvents = mEditResponseHelper.getWhichEvents(); 640 switch (whichEvents) { 641 case -1: 642 return false; 643 case UPDATE_SINGLE: 644 createExceptionResponse(mEventId, mCalendarOwnerAttendeeId, status); 645 return true; 646 case UPDATE_ALL: 647 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 648 return true; 649 default: 650 Log.e(TAG, "Unexpected choice for updating invitation response"); 651 break; 652 } 653 return false; 654 } 655 656 private void updateResponse(long eventId, long attendeeId, int status) { 657 // Update the attendee status in the attendees table. the provider 658 // takes care of updating the self attendance status. 659 ContentValues values = new ContentValues(); 660 661 if (!TextUtils.isEmpty(mCalendarOwnerAccount)) { 662 values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount); 663 } 664 values.put(Attendees.ATTENDEE_STATUS, status); 665 values.put(Attendees.EVENT_ID, eventId); 666 667 Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId); 668 669 mHandler.startUpdate(mHandler.getNextToken(), null, uri, values, 670 null, null, Utils.UNDO_DELAY); 671 } 672 673 private void createExceptionResponse(long eventId, long attendeeId, 674 int status) { 675 if (mEventCursor == null || !mEventCursor.moveToFirst()) { 676 return; 677 } 678 // TODO change this fragment to build a CalendarEventModel and save via 679 // EditEventHelper 680 681 ContentValues values = new ContentValues(); 682 683 String title = mEventCursor.getString(EVENT_INDEX_TITLE); 684 String timezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); 685 int calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID); 686 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 687 String syncId = mEventCursor.getString(EVENT_INDEX_SYNC_ID); 688 689 values.put(Events.TITLE, title); 690 values.put(Events.EVENT_TIMEZONE, timezone); 691 values.put(Events.ALL_DAY, allDay ? 1 : 0); 692 values.put(Events.CALENDAR_ID, calendarId); 693 values.put(Events.DTSTART, mStartMillis); 694 values.put(Events.DTEND, mEndMillis); 695 values.put(Events.ORIGINAL_EVENT, syncId); 696 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis); 697 values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0); 698 values.put(Events.STATUS, Events.STATUS_CONFIRMED); 699 values.put(Events.SELF_ATTENDEE_STATUS, status); 700 701 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 702 int eventIdIndex = ops.size(); 703 704 ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values).build()); 705 706 if (mHasAttendeeData) { 707 ContentProviderOperation.Builder b; 708 // Insert the new attendees 709 for (Attendee attendee : mAcceptedAttendees) { 710 addAttendee( 711 values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_ACCEPTED); 712 } 713 for (Attendee attendee : mDeclinedAttendees) { 714 addAttendee( 715 values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_DECLINED); 716 } 717 for (Attendee attendee : mTentativeAttendees) { 718 addAttendee( 719 values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_TENTATIVE); 720 } 721 for (Attendee attendee : mNoResponseAttendees) { 722 addAttendee(values, ops, eventIdIndex, attendee, Attendees.ATTENDEE_STATUS_NONE); 723 } 724 } 725 726 // Create a recurrence exception 727 mHandler.startBatch( 728 mHandler.getNextToken(), null, Calendar.AUTHORITY, ops, Utils.UNDO_DELAY); 729 } 730 731 /** 732 * @param values 733 * @param ops 734 * @param eventIdIndex 735 * @param attendee 736 */ 737 private void addAttendee(ContentValues values, ArrayList<ContentProviderOperation> ops, 738 int eventIdIndex, Attendee attendee, int attendeeStatus) { 739 ContentProviderOperation.Builder b; 740 values.clear(); 741 values.put(Attendees.ATTENDEE_NAME, attendee.mName); 742 values.put(Attendees.ATTENDEE_EMAIL, attendee.mEmail); 743 values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE); 744 values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE); 745 values.put(Attendees.ATTENDEE_STATUS, attendeeStatus); 746 747 b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI).withValues(values); 748 b.withValueBackReference(Attendees.EVENT_ID, eventIdIndex); 749 ops.add(b.build()); 750 } 751 752 public static int getResponseFromButtonId(int buttonId) { 753 int response; 754 switch (buttonId) { 755 case R.id.response_yes: 756 response = Attendees.ATTENDEE_STATUS_ACCEPTED; 757 break; 758 case R.id.response_maybe: 759 response = Attendees.ATTENDEE_STATUS_TENTATIVE; 760 break; 761 case R.id.response_no: 762 response = Attendees.ATTENDEE_STATUS_DECLINED; 763 break; 764 default: 765 response = Attendees.ATTENDEE_STATUS_NONE; 766 } 767 return response; 768 } 769 770 public static int findButtonIdForResponse(int response) { 771 int buttonId; 772 switch (response) { 773 case Attendees.ATTENDEE_STATUS_ACCEPTED: 774 buttonId = R.id.response_yes; 775 break; 776 case Attendees.ATTENDEE_STATUS_TENTATIVE: 777 buttonId = R.id.response_maybe; 778 break; 779 case Attendees.ATTENDEE_STATUS_DECLINED: 780 buttonId = R.id.response_no; 781 break; 782 default: 783 buttonId = -1; 784 } 785 return buttonId; 786 } 787 788 private void doEdit() { 789 Context c = getActivity(); 790 // This ensures that we aren't in the process of closing and have been 791 // unattached already 792 if (c != null) { 793 CalendarController.getInstance(c).sendEventRelatedEvent( 794 this, EventType.VIEW_EVENT_DETAILS, mEventId, mStartMillis, mEndMillis, 0, 0, -1); 795 } 796 } 797 798 private void updateEvent(View view) { 799 if (mEventCursor == null) { 800 return; 801 } 802 803 String eventName = mEventCursor.getString(EVENT_INDEX_TITLE); 804 if (eventName == null || eventName.length() == 0) { 805 eventName = getActivity().getString(R.string.no_title_label); 806 } 807 808 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 809 String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION); 810 String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION); 811 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 812 String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); 813 mColor = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff; 814 815 view.findViewById(R.id.color).setBackgroundColor(mColor); 816 817 TextView title = mTitle; 818 819// View divider = view.findViewById(R.id.divider); 820// divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN); 821 822 // What 823 if (eventName != null) { 824 setTextCommon(view, R.id.title, eventName); 825 } 826 827 // When 828 String when; 829 int flags = DateUtils.FORMAT_SHOW_DATE; 830 if (allDay) { 831 flags |= DateUtils.FORMAT_UTC | DateUtils.FORMAT_SHOW_WEEKDAY; 832 } else { 833 flags |= DateUtils.FORMAT_SHOW_TIME; 834 if (DateFormat.is24HourFormat(getActivity())) { 835 flags |= DateUtils.FORMAT_24HOUR; 836 } 837 } 838 when = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flags); 839 setTextCommon(view, R.id.when, when); 840 841//CLEANUP // Show the event timezone if it is different from the local timezone 842// Time time = new Time(); 843// String localTimezone = time.timezone; 844// if (allDay) { 845// localTimezone = Time.TIMEZONE_UTC; 846// } 847// if (eventTimezone != null && !localTimezone.equals(eventTimezone) && !allDay) { 848// String displayName; 849// TimeZone tz = TimeZone.getTimeZone(localTimezone); 850// if (tz == null || tz.getID().equals("GMT")) { 851// displayName = localTimezone; 852// } else { 853// displayName = tz.getDisplayName(); 854// } 855// 856// setTextCommon(view, R.id.timezone, displayName); 857// setVisibilityCommon(view, R.id.timezone_container, View.VISIBLE); 858// } else { 859// setVisibilityCommon(view, R.id.timezone_container, View.GONE); 860// } 861 862 // Repeat 863 if (!TextUtils.isEmpty(rRule)) { 864 EventRecurrence eventRecurrence = new EventRecurrence(); 865 eventRecurrence.parse(rRule); 866 Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater)); 867 if (allDay) { 868 date.timezone = Time.TIMEZONE_UTC; 869 } 870 date.set(mStartMillis); 871 eventRecurrence.setStartDate(date); 872 String repeatString = EventRecurrenceFormatter.getRepeatString( 873 getActivity().getResources(), eventRecurrence); 874 setTextCommon(view, R.id.repeat, repeatString); 875 setVisibilityCommon(view, R.id.repeat_container, View.VISIBLE); 876 } else { 877 setVisibilityCommon(view, R.id.repeat_container, View.GONE); 878 } 879 880 // Where 881 if (location == null || location.trim().length() == 0) { 882 setVisibilityCommon(view, R.id.where, View.GONE); 883 } else { 884 final TextView textView = mWhere; 885 if (textView != null) { 886 textView.setAutoLinkMask(0); 887 textView.setText(location.trim()); 888 if (!Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES 889 | Linkify.MAP_ADDRESSES)) { 890 Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); 891 } 892 textView.setOnTouchListener(new OnTouchListener() { 893 public boolean onTouch(View v, MotionEvent event) { 894 try { 895 return v.onTouchEvent(event); 896 } catch (ActivityNotFoundException e) { 897 // ignore 898 return true; 899 } 900 } 901 }); 902 } 903 } 904 905 // Description 906 if (description != null && description.length() != 0) { 907 setTextCommon(view, R.id.description, description); 908 } 909 } 910 911 private void sendAccessibilityEvent() { 912 AccessibilityManager am = AccessibilityManager.getInstance(getActivity()); 913 if (!am.isEnabled()) { 914 return; 915 } 916 917 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 918 event.setClassName(getClass().getName()); 919 event.setPackageName(getActivity().getPackageName()); 920 List<CharSequence> text = event.getText(); 921 922 addFieldToAccessibilityEvent(text, mTitle); 923 addFieldToAccessibilityEvent(text, mCalendar); 924 addFieldToAccessibilityEvent(text, mWhen); 925 addFieldToAccessibilityEvent(text, mWhere); 926 addFieldToAccessibilityEvent(text, mWhat); 927 addFieldToAccessibilityEvent(text, mAttendees); 928 929 RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value); 930 if (response.getVisibility() == View.VISIBLE) { 931 int id = response.getCheckedRadioButtonId(); 932 if (id != View.NO_ID) { 933 text.add(((TextView) getView().findViewById(R.id.response_label)).getText()); 934 text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE)); 935 } 936 } 937 938 am.sendAccessibilityEvent(event); 939 } 940 941 /** 942 * @param text 943 */ 944 private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView view) { 945 String str = view.toString().trim(); 946 if (!TextUtils.isEmpty(str)) { 947 text.add(mTitle.getText()); 948 text.add(PERIOD_SPACE); 949 } 950 } 951 952 private void updateCalendar(View view) { 953 mCalendarOwnerAccount = ""; 954 if (mCalendarsCursor != null && mEventCursor != null) { 955 mCalendarsCursor.moveToFirst(); 956 String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 957 mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount; 958 mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0; 959 960 String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 961 962 // start duplicate calendars query 963 mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI, 964 CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE, 965 new String[] {displayName}, null); 966 967 String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER); 968 mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer); 969 mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0; 970 mCanModifyCalendar = 971 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CONTRIBUTOR_ACCESS; 972 mIsBusyFreeCalendar = 973 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.FREEBUSY_ACCESS; 974 975 if (!mIsBusyFreeCalendar) { 976 Button b = (Button) mView.findViewById(R.id.edit); 977 b.setEnabled(true); 978 b.setOnClickListener(new OnClickListener() { 979 @Override 980 public void onClick(View v) { 981 doEdit(); 982 EventInfoFragment.this.dismiss(); 983 } 984 }); 985 } 986 if (!mCanModifyCalendar) { 987 mView.findViewById(R.id.delete).setEnabled(false); 988 } 989 } else { 990 setVisibilityCommon(view, R.id.calendar, View.GONE); 991 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); 992 } 993 } 994 995 private void updateAttendees(View view) { 996 TextView tv = mAttendees; 997 SpannableStringBuilder sb = new SpannableStringBuilder(); 998 formatAttendees(mAcceptedAttendees, sb, Attendees.ATTENDEE_STATUS_ACCEPTED); 999 formatAttendees(mDeclinedAttendees, sb, Attendees.ATTENDEE_STATUS_DECLINED); 1000 formatAttendees(mTentativeAttendees, sb, Attendees.ATTENDEE_STATUS_TENTATIVE); 1001 formatAttendees(mNoResponseAttendees, sb, Attendees.ATTENDEE_STATUS_NONE); 1002 1003 if (sb.length() > 0) { 1004 // Add the label after the attendees are formatted because 1005 // formatAttendees would prepend ", " if sb.length != 0 1006 String label = getActivity().getResources().getString(R.string.attendees_label); 1007 sb.insert(0, label); 1008 sb.insert(label.length(), " "); 1009 sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(), 1010 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1011 1012 tv.setText(sb); 1013 } 1014 } 1015 1016 private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) { 1017 if (attendees.size() <= 0) { 1018 return; 1019 } 1020 1021 int begin = sb.length(); 1022 boolean firstTime = sb.length() == 0; 1023 1024 if (firstTime == false) { 1025 begin += 2; // skip over the ", " for formatting. 1026 } 1027 1028 for (Attendee attendee : attendees) { 1029 if (firstTime) { 1030 firstTime = false; 1031 } else { 1032 sb.append(", "); 1033 } 1034 1035 String name = attendee.getDisplayName(); 1036 sb.append(name); 1037 } 1038 1039 switch (type) { 1040 case Attendees.ATTENDEE_STATUS_ACCEPTED: 1041 break; 1042 case Attendees.ATTENDEE_STATUS_DECLINED: 1043 sb.setSpan(new StrikethroughSpan(), begin, sb.length(), 1044 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1045 // fall through 1046 default: 1047 // The last INCLUSIVE causes the foreground color to be applied 1048 // to the rest of the span. If not, the comma at the end of the 1049 // declined or tentative may be black. 1050 sb.setSpan(new ForegroundColorSpan(0xFF999999), begin, sb.length(), 1051 Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 1052 break; 1053 } 1054 } 1055 1056 void updateResponse(View view) { 1057 // we only let the user accept/reject/etc. a meeting if: 1058 // a) you can edit the event's containing calendar AND 1059 // b) you're not the organizer and only attendee AND 1060 // c) organizerCanRespond is enabled for the calendar 1061 // (if the attendee data has been hidden, the visible number of attendees 1062 // will be 1 -- the calendar owner's). 1063 // (there are more cases involved to be 100% accurate, such as 1064 // paying attention to whether or not an attendee status was 1065 // included in the feed, but we're currently omitting those corner cases 1066 // for simplicity). 1067 1068 // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel. 1069 if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) || 1070 (mIsOrganizer && !mOwnerCanRespond)) { 1071 setVisibilityCommon(view, R.id.response_container, View.GONE); 1072 return; 1073 } 1074 1075 setVisibilityCommon(view, R.id.response_container, View.VISIBLE); 1076 1077 1078 int response; 1079 if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) { 1080 response = mAttendeeResponseFromIntent; 1081 } else { 1082 response = mOriginalAttendeeResponse; 1083 } 1084 1085 int buttonToCheck = findButtonIdForResponse(response); 1086 RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value); 1087 radioGroup.check(buttonToCheck); // -1 clear all radio buttons 1088 radioGroup.setOnCheckedChangeListener(this); 1089 } 1090 1091 private void setTextCommon(View view, int id, CharSequence text) { 1092 TextView textView = (TextView) view.findViewById(id); 1093 if (textView == null) 1094 return; 1095 textView.setText(text); 1096 } 1097 1098 private void setVisibilityCommon(View view, int id, int visibility) { 1099 View v = view.findViewById(id); 1100 if (v != null) { 1101 v.setVisibility(visibility); 1102 } 1103 return; 1104 } 1105 1106 /** 1107 * Taken from com.google.android.gm.HtmlConversationActivity 1108 * 1109 * Send the intent that shows the Contact info corresponding to the email address. 1110 */ 1111 public void showContactInfo(Attendee attendee, Rect rect) { 1112 // First perform lookup query to find existing contact 1113 final ContentResolver resolver = getActivity().getContentResolver(); 1114 final String address = attendee.mEmail; 1115 final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI, 1116 Uri.encode(address)); 1117 final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri); 1118 1119 if (lookupUri != null) { 1120 // Found matching contact, trigger QuickContact 1121 QuickContact.showQuickContact(getActivity(), rect, lookupUri, 1122 QuickContact.MODE_MEDIUM, null); 1123 } else { 1124 // No matching contact, ask user to create one 1125 final Uri mailUri = Uri.fromParts("mailto", address, null); 1126 final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri); 1127 1128 // Pass along full E-mail string for possible create dialog 1129 Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null); 1130 intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString()); 1131 1132 // Only provide personal name hint if we have one 1133 final String senderPersonal = attendee.mName; 1134 if (!TextUtils.isEmpty(senderPersonal)) { 1135 intent.putExtra(Intents.Insert.NAME, senderPersonal); 1136 } 1137 1138 startActivity(intent); 1139 } 1140 } 1141 1142 @Override 1143 public void onPause() { 1144 mIsPaused = true; 1145 mHandler.removeCallbacks(onDeleteRunnable); 1146 super.onPause(); 1147 } 1148 1149 @Override 1150 public void onResume() { 1151 super.onResume(); 1152 mIsPaused = false; 1153 if (mDismissOnResume) { 1154 mHandler.post(onDeleteRunnable); 1155 } 1156 } 1157 1158 @Override 1159 public void eventsChanged() { 1160 } 1161 1162 @Override 1163 public long getSupportedEventTypes() { 1164 return EventType.EVENTS_CHANGED; 1165 } 1166 1167 @Override 1168 public void handleEvent(EventInfo event) { 1169 if (event.eventType == EventType.EVENTS_CHANGED) { 1170 // reload the data 1171 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 1172 null, null, null); 1173 } 1174 1175 } 1176} 1177