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