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