EventInfoFragment.java revision 80d640f3edc455d053a0e71a419f222a6f03385e
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.calendar; 18 19import com.android.calendar.CalendarController.EventInfo; 20import com.android.calendar.CalendarController.EventType; 21import com.android.calendar.CalendarEventModel.Attendee; 22import com.android.calendar.CalendarEventModel.ReminderEntry; 23import com.android.calendar.event.AttendeesView; 24import com.android.calendar.event.EditEventActivity; 25import com.android.calendar.event.EditEventHelper; 26import com.android.calendar.event.EventViewUtils; 27import com.android.calendarcommon.EventRecurrence; 28 29import android.animation.Animator; 30import android.animation.AnimatorListenerAdapter; 31import android.animation.ObjectAnimator; 32import android.app.Activity; 33import android.app.Dialog; 34import android.app.DialogFragment; 35import android.app.Service; 36import android.content.ActivityNotFoundException; 37import android.content.ContentProviderOperation; 38import android.content.ContentResolver; 39import android.content.ContentUris; 40import android.content.ContentValues; 41import android.content.Context; 42import android.content.DialogInterface; 43import android.content.Intent; 44import android.content.SharedPreferences; 45import android.content.res.Resources; 46import android.database.Cursor; 47import android.graphics.Rect; 48import android.graphics.Typeface; 49import android.net.Uri; 50import android.os.Bundle; 51import android.provider.CalendarContract; 52import android.provider.CalendarContract.Attendees; 53import android.provider.CalendarContract.Calendars; 54import android.provider.CalendarContract.Events; 55import android.provider.CalendarContract.Reminders; 56import android.provider.ContactsContract; 57import android.provider.ContactsContract.CommonDataKinds; 58import android.provider.ContactsContract.Intents; 59import android.provider.ContactsContract.QuickContact; 60import android.text.Spannable; 61import android.text.SpannableString; 62import android.text.SpannableStringBuilder; 63import android.text.Spanned; 64import android.text.TextUtils; 65import android.text.format.Time; 66import android.text.method.LinkMovementMethod; 67import android.text.method.MovementMethod; 68import android.text.style.ForegroundColorSpan; 69import android.text.style.StyleSpan; 70import android.text.style.URLSpan; 71import android.text.util.Linkify; 72import android.text.util.Rfc822Token; 73import android.util.Log; 74import android.view.Gravity; 75import android.view.LayoutInflater; 76import android.view.Menu; 77import android.view.MenuInflater; 78import android.view.MenuItem; 79import android.view.MotionEvent; 80import android.view.View; 81import android.view.View.OnClickListener; 82import android.view.View.OnTouchListener; 83import android.view.ViewGroup; 84import android.view.Window; 85import android.view.WindowManager; 86import android.view.accessibility.AccessibilityEvent; 87import android.view.accessibility.AccessibilityManager; 88import android.widget.AdapterView; 89import android.widget.AdapterView.OnItemSelectedListener; 90import android.widget.Button; 91import android.widget.LinearLayout; 92import android.widget.RadioButton; 93import android.widget.RadioGroup; 94import android.widget.RadioGroup.OnCheckedChangeListener; 95import android.widget.ScrollView; 96import android.widget.TextView; 97import android.widget.Toast; 98 99import java.util.ArrayList; 100import java.util.Arrays; 101import java.util.Collections; 102import java.util.List; 103import java.util.regex.Pattern; 104 105import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; 106import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; 107import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; 108import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH; 109 110 111public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener, 112 CalendarController.EventHandler, OnClickListener, DeleteEventHelper.DeleteNotifyListener { 113 public static final boolean DEBUG = false; 114 115 public static final String TAG = "EventInfoFragment"; 116 117 protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; 118 protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis"; 119 protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis"; 120 protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog"; 121 protected static final String BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible"; 122 protected static final String BUNDLE_KEY_WINDOW_STYLE = "key_window_style"; 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 // Style of view 135 public static final int FULL_WINDOW_STYLE = 0; 136 public static final int DIALOG_WINDOW_STYLE = 1; 137 138 private int mWindowStyle = DIALOG_WINDOW_STYLE; 139 140 // Query tokens for QueryHandler 141 private static final int TOKEN_QUERY_EVENT = 1 << 0; 142 private static final int TOKEN_QUERY_CALENDARS = 1 << 1; 143 private static final int TOKEN_QUERY_ATTENDEES = 1 << 2; 144 private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3; 145 private static final int TOKEN_QUERY_REMINDERS = 1 << 4; 146 private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS 147 | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT 148 | TOKEN_QUERY_REMINDERS; 149 private int mCurrentQuery = 0; 150 151 private static final String[] EVENT_PROJECTION = new String[] { 152 Events._ID, // 0 do not remove; used in DeleteEventHelper 153 Events.TITLE, // 1 do not remove; used in DeleteEventHelper 154 Events.RRULE, // 2 do not remove; used in DeleteEventHelper 155 Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper 156 Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper 157 Events.DTSTART, // 5 do not remove; used in DeleteEventHelper 158 Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper 159 Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper 160 Events.DESCRIPTION, // 8 161 Events.EVENT_LOCATION, // 9 162 Calendars.CALENDAR_ACCESS_LEVEL, // 10 163 Events.DISPLAY_COLOR, // 11 164 Events.HAS_ATTENDEE_DATA, // 12 165 Events.ORGANIZER, // 13 166 Events.HAS_ALARM, // 14 167 Calendars.MAX_REMINDERS, //15 168 Calendars.ALLOWED_REMINDERS, // 16 169 Events.ORIGINAL_SYNC_ID, // 17 do not remove; used in DeleteEventHelper 170 }; 171 private static final int EVENT_INDEX_ID = 0; 172 private static final int EVENT_INDEX_TITLE = 1; 173 private static final int EVENT_INDEX_RRULE = 2; 174 private static final int EVENT_INDEX_ALL_DAY = 3; 175 private static final int EVENT_INDEX_CALENDAR_ID = 4; 176 private static final int EVENT_INDEX_SYNC_ID = 6; 177 private static final int EVENT_INDEX_EVENT_TIMEZONE = 7; 178 private static final int EVENT_INDEX_DESCRIPTION = 8; 179 private static final int EVENT_INDEX_EVENT_LOCATION = 9; 180 private static final int EVENT_INDEX_ACCESS_LEVEL = 10; 181 private static final int EVENT_INDEX_COLOR = 11; 182 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12; 183 private static final int EVENT_INDEX_ORGANIZER = 13; 184 private static final int EVENT_INDEX_HAS_ALARM = 14; 185 private static final int EVENT_INDEX_MAX_REMINDERS = 15; 186 private static final int EVENT_INDEX_ALLOWED_REMINDERS = 16; 187 188 189 private static final String[] ATTENDEES_PROJECTION = new String[] { 190 Attendees._ID, // 0 191 Attendees.ATTENDEE_NAME, // 1 192 Attendees.ATTENDEE_EMAIL, // 2 193 Attendees.ATTENDEE_RELATIONSHIP, // 3 194 Attendees.ATTENDEE_STATUS, // 4 195 }; 196 private static final int ATTENDEES_INDEX_ID = 0; 197 private static final int ATTENDEES_INDEX_NAME = 1; 198 private static final int ATTENDEES_INDEX_EMAIL = 2; 199 private static final int ATTENDEES_INDEX_RELATIONSHIP = 3; 200 private static final int ATTENDEES_INDEX_STATUS = 4; 201 202 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; 203 204 private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, " 205 + Attendees.ATTENDEE_EMAIL + " ASC"; 206 207 private static final String[] REMINDERS_PROJECTION = new String[] { 208 Reminders._ID, // 0 209 Reminders.MINUTES, // 1 210 Reminders.METHOD // 2 211 }; 212 private static final int REMINDERS_INDEX_ID = 0; 213 private static final int REMINDERS_MINUTES_ID = 1; 214 private static final int REMINDERS_METHOD_ID = 2; 215 216 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?"; 217 218 static final String[] CALENDARS_PROJECTION = new String[] { 219 Calendars._ID, // 0 220 Calendars.CALENDAR_DISPLAY_NAME, // 1 221 Calendars.OWNER_ACCOUNT, // 2 222 Calendars.CAN_ORGANIZER_RESPOND, // 3 223 Calendars.ACCOUNT_NAME // 4 224 }; 225 static final int CALENDARS_INDEX_DISPLAY_NAME = 1; 226 static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 227 static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3; 228 static final int CALENDARS_INDEX_ACCOUNT_NAME = 4; 229 230 static final String CALENDARS_WHERE = Calendars._ID + "=?"; 231 static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?"; 232 233 private static final String NANP_ALLOWED_SYMBOLS = "()+-*#."; 234 private static final int NANP_MIN_DIGITS = 7; 235 private static final int NANP_MAX_DIGITS = 11; 236 237 238 private View mView; 239 240 private Uri mUri; 241 private long mEventId; 242 private Cursor mEventCursor; 243 private Cursor mAttendeesCursor; 244 private Cursor mCalendarsCursor; 245 private Cursor mRemindersCursor; 246 247 private static float mScale = 0; // Used for supporting different screen densities 248 249 private long mStartMillis; 250 private long mEndMillis; 251 private boolean mAllDay; 252 253 private boolean mHasAttendeeData; 254 private String mEventOrganizer; 255 private boolean mIsOrganizer; 256 private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 257 private boolean mOwnerCanRespond; 258 private String mSyncAccountName; 259 private String mCalendarOwnerAccount; 260 private boolean mCanModifyCalendar; 261 private boolean mCanModifyEvent; 262 private boolean mIsBusyFreeCalendar; 263 private int mNumOfAttendees; 264 265 private EditResponseHelper mEditResponseHelper; 266 private boolean mDeleteDialogVisible = false; 267 private DeleteEventHelper mDeleteHelper; 268 269 private int mOriginalAttendeeResponse; 270 private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE; 271 private int mUserSetResponse = CalendarController.ATTENDEE_NO_RESPONSE; 272 private boolean mIsRepeating; 273 private boolean mHasAlarm; 274 private int mMaxReminders; 275 private String mCalendarAllowedReminders; 276 // Used to prevent saving changes in event if it is being deleted. 277 private boolean mEventDeletionStarted = false; 278 279 private TextView mTitle; 280 private TextView mWhenDateTime; 281 private TextView mWhere; 282 private ExpandableTextView mDesc; 283 private AttendeesView mLongAttendees; 284 private Menu mMenu = null; 285 private View mHeadlines; 286 private ScrollView mScrollView; 287 private View mLoadingMsgView; 288 private ObjectAnimator mAnimateAlpha; 289 private long mLoadingMsgStartTime; 290 private static final int FADE_IN_TIME = 300; // in milliseconds 291 private static final int LOADING_MSG_DELAY = 600; // in milliseconds 292 private static final int LOADING_MSG_MIN_DISPLAY_TIME = 600; 293 private boolean mNoCrossFade = false; // Used to prevent repeated cross-fade 294 295 296 private static final Pattern mWildcardPattern = Pattern.compile("^.*$"); 297 298 ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>(); 299 ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>(); 300 ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>(); 301 ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>(); 302 ArrayList<String> mToEmails = new ArrayList<String>(); 303 ArrayList<String> mCcEmails = new ArrayList<String>(); 304 private int mColor; 305 306 307 private int mDefaultReminderMinutes; 308 private final ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0); 309 public ArrayList<ReminderEntry> mReminders; 310 public ArrayList<ReminderEntry> mOriginalReminders = new ArrayList<ReminderEntry>(); 311 public ArrayList<ReminderEntry> mUnsupportedReminders = new ArrayList<ReminderEntry>(); 312 private boolean mUserModifiedReminders = false; 313 314 /** 315 * Contents of the "minutes" spinner. This has default values from the XML file, augmented 316 * with any additional values that were already associated with the event. 317 */ 318 private ArrayList<Integer> mReminderMinuteValues; 319 private ArrayList<String> mReminderMinuteLabels; 320 321 /** 322 * Contents of the "methods" spinner. The "values" list specifies the method constant 323 * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels. Any methods that 324 * aren't allowed by the Calendar will be removed. 325 */ 326 private ArrayList<Integer> mReminderMethodValues; 327 private ArrayList<String> mReminderMethodLabels; 328 329 private QueryHandler mHandler; 330 331 332 private final Runnable mTZUpdater = new Runnable() { 333 @Override 334 public void run() { 335 updateEvent(mView); 336 } 337 }; 338 339 private final Runnable mLoadingMsgAlphaUpdater = new Runnable() { 340 @Override 341 public void run() { 342 // Since this is run after a delay, make sure to only show the message 343 // if the event's data is not shown yet. 344 if (!mAnimateAlpha.isRunning() && mScrollView.getAlpha() == 0) { 345 mLoadingMsgStartTime = System.currentTimeMillis(); 346 mLoadingMsgView.setAlpha(1); 347 } 348 } 349 }; 350 351 private OnItemSelectedListener mReminderChangeListener; 352 353 private static int mDialogWidth = 500; 354 private static int mDialogHeight = 600; 355 private static int DIALOG_TOP_MARGIN = 8; 356 private boolean mIsDialog = false; 357 private boolean mIsPaused = true; 358 private boolean mDismissOnResume = false; 359 private int mX = -1; 360 private int mY = -1; 361 private int mMinTop; // Dialog cannot be above this location 362 private boolean mIsTabletConfig; 363 private Activity mActivity; 364 private Context mContext; 365 366 private class QueryHandler extends AsyncQueryService { 367 public QueryHandler(Context context) { 368 super(context); 369 } 370 371 @Override 372 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 373 // if the activity is finishing, then close the cursor and return 374 final Activity activity = getActivity(); 375 if (activity == null || activity.isFinishing()) { 376 cursor.close(); 377 return; 378 } 379 380 switch (token) { 381 case TOKEN_QUERY_EVENT: 382 mEventCursor = Utils.matrixCursorFromCursor(cursor); 383 if (initEventCursor()) { 384 // The cursor is empty. This can happen if the event was 385 // deleted. 386 // FRAG_TODO we should no longer rely on Activity.finish() 387 activity.finish(); 388 return; 389 } 390 updateEvent(mView); 391 prepareReminders(); 392 393 // start calendar query 394 Uri uri = Calendars.CONTENT_URI; 395 String[] args = new String[] { 396 Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))}; 397 startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION, 398 CALENDARS_WHERE, args, null); 399 break; 400 case TOKEN_QUERY_CALENDARS: 401 mCalendarsCursor = Utils.matrixCursorFromCursor(cursor); 402 updateCalendar(mView); 403 // FRAG_TODO fragments shouldn't set the title anymore 404 updateTitle(); 405 406 if (!mIsBusyFreeCalendar) { 407 args = new String[] { Long.toString(mEventId) }; 408 409 // start attendees query 410 uri = Attendees.CONTENT_URI; 411 startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION, 412 ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER); 413 } else { 414 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES); 415 } 416 if (mHasAlarm) { 417 // start reminders query 418 args = new String[] { Long.toString(mEventId) }; 419 uri = Reminders.CONTENT_URI; 420 startQuery(TOKEN_QUERY_REMINDERS, null, uri, 421 REMINDERS_PROJECTION, REMINDERS_WHERE, args, null); 422 } else { 423 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS); 424 } 425 break; 426 case TOKEN_QUERY_ATTENDEES: 427 mAttendeesCursor = Utils.matrixCursorFromCursor(cursor); 428 initAttendeesCursor(mView); 429 updateResponse(mView); 430 break; 431 case TOKEN_QUERY_REMINDERS: 432 mRemindersCursor = Utils.matrixCursorFromCursor(cursor); 433 initReminders(mView, mRemindersCursor); 434 break; 435 case TOKEN_QUERY_DUPLICATE_CALENDARS: 436 Resources res = activity.getResources(); 437 SpannableStringBuilder sb = new SpannableStringBuilder(); 438 439 // Label 440 String label = res.getString(R.string.view_event_calendar_label); 441 sb.append(label).append(" "); 442 sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(), 443 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 444 445 // Calendar display name 446 String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 447 sb.append(calendarName); 448 449 // Show email account if display name is not unique and 450 // display name != email 451 String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 452 if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) { 453 sb.append(" (").append(email).append(")"); 454 } 455 456 break; 457 } 458 cursor.close(); 459 sendAccessibilityEventIfQueryDone(token); 460 // All queries are done, show the view 461 if (mCurrentQuery == TOKEN_QUERY_ALL) { 462 if (mLoadingMsgView.getAlpha() == 1) { 463 // Loading message is showing, let it stay a bit more (to prevent 464 // flashing) by adding a start delay to the event animation 465 long timeDiff = LOADING_MSG_MIN_DISPLAY_TIME - (System.currentTimeMillis() - 466 mLoadingMsgStartTime); 467 if (timeDiff > 0) { 468 mAnimateAlpha.setStartDelay(timeDiff); 469 } 470 } 471 if (!mAnimateAlpha.isRunning() &&!mAnimateAlpha.isStarted() && !mNoCrossFade) { 472 mAnimateAlpha.start(); 473 } else { 474 mScrollView.setAlpha(1); 475 mLoadingMsgView.setVisibility(View.GONE); 476 } 477 } 478 } 479 } 480 481 private void sendAccessibilityEventIfQueryDone(int token) { 482 mCurrentQuery |= token; 483 if (mCurrentQuery == TOKEN_QUERY_ALL) { 484 sendAccessibilityEvent(); 485 } 486 } 487 488 public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, 489 int attendeeResponse, boolean isDialog, int windowStyle) { 490 491 if (isDialog) { 492 Resources r = context.getResources(); 493 494 mDialogWidth = r.getInteger(R.integer.event_info_dialog_width); 495 mDialogHeight = r.getInteger(R.integer.event_info_dialog_height); 496 497 if (mScale == 0) { 498 mScale = context.getResources().getDisplayMetrics().density; 499 if (mScale != 1) { 500 mDialogWidth *= mScale; 501 mDialogHeight *= mScale; 502 DIALOG_TOP_MARGIN *= mScale; 503 } 504 } 505 } 506 mIsDialog = isDialog; 507 508 setStyle(DialogFragment.STYLE_NO_TITLE, 0); 509 mUri = uri; 510 mStartMillis = startMillis; 511 mEndMillis = endMillis; 512 mAttendeeResponseFromIntent = attendeeResponse; 513 mWindowStyle = windowStyle; 514 } 515 516 // This is currently required by the fragment manager. 517 public EventInfoFragment() { 518 } 519 520 521 522 public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, 523 int attendeeResponse, boolean isDialog, int windowStyle) { 524 this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis, 525 endMillis, attendeeResponse, isDialog, windowStyle); 526 mEventId = eventId; 527 } 528 529 @Override 530 public void onActivityCreated(Bundle savedInstanceState) { 531 super.onActivityCreated(savedInstanceState); 532 533 mReminderChangeListener = new OnItemSelectedListener() { 534 @Override 535 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 536 Integer prevValue = (Integer) parent.getTag(); 537 if (prevValue == null || prevValue != position) { 538 parent.setTag(position); 539 mUserModifiedReminders = true; 540 } 541 } 542 543 @Override 544 public void onNothingSelected(AdapterView<?> parent) { 545 // do nothing 546 } 547 548 }; 549 550 if (savedInstanceState != null) { 551 mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false); 552 mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE, 553 DIALOG_WINDOW_STYLE); 554 } 555 556 if (mIsDialog) { 557 applyDialogParams(); 558 } 559 mContext = getActivity(); 560 } 561 562 private void applyDialogParams() { 563 Dialog dialog = getDialog(); 564 dialog.setCanceledOnTouchOutside(true); 565 566 Window window = dialog.getWindow(); 567 window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 568 569 WindowManager.LayoutParams a = window.getAttributes(); 570 a.dimAmount = .4f; 571 572 a.width = mDialogWidth; 573 a.height = mDialogHeight; 574 575 576 // On tablets , do smart positioning of dialog 577 // On phones , use the whole screen 578 579 if (mX != -1 || mY != -1) { 580 a.x = mX - mDialogWidth / 2; 581 a.y = mY - mDialogHeight / 2; 582 if (a.y < mMinTop) { 583 a.y = mMinTop + DIALOG_TOP_MARGIN; 584 } 585 a.gravity = Gravity.LEFT | Gravity.TOP; 586 } 587 window.setAttributes(a); 588 } 589 590 public void setDialogParams(int x, int y, int minTop) { 591 mX = x; 592 mY = y; 593 mMinTop = minTop; 594 } 595 596 // Implements OnCheckedChangeListener 597 @Override 598 public void onCheckedChanged(RadioGroup group, int checkedId) { 599 // If this is not a repeating event, then don't display the dialog 600 // asking which events to change. 601 mUserSetResponse = getResponseFromButtonId(checkedId); 602 if (!mIsRepeating) { 603 return; 604 } 605 606 // If the selection is the same as the original, then don't display the 607 // dialog asking which events to change. 608 if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) { 609 return; 610 } 611 612 // This is a repeating event. We need to ask the user if they mean to 613 // change just this one instance or all instances. 614 mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents()); 615 } 616 617 public void onNothingSelected(AdapterView<?> parent) { 618 } 619 620 @Override 621 public void onAttach(Activity activity) { 622 super.onAttach(activity); 623 mActivity = activity; 624 mEditResponseHelper = new EditResponseHelper(activity); 625 626 if (mAttendeeResponseFromIntent != Attendees.ATTENDEE_STATUS_NONE) { 627 mEditResponseHelper.setWhichEvents(UPDATE_ALL); 628 } 629 mHandler = new QueryHandler(activity); 630 if (!mIsDialog) { 631 setHasOptionsMenu(true); 632 } 633 } 634 635 @Override 636 public View onCreateView(LayoutInflater inflater, ViewGroup container, 637 Bundle savedInstanceState) { 638 639 if (savedInstanceState != null) { 640 mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false); 641 mWindowStyle = savedInstanceState.getInt(BUNDLE_KEY_WINDOW_STYLE, 642 DIALOG_WINDOW_STYLE); 643 mDeleteDialogVisible = 644 savedInstanceState.getBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE,false); 645 646 } 647 648 if (mWindowStyle == DIALOG_WINDOW_STYLE) { 649 mView = inflater.inflate(R.layout.event_info_dialog, container, false); 650 } else { 651 mView = inflater.inflate(R.layout.event_info, container, false); 652 } 653 mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view); 654 mLoadingMsgView = mView.findViewById(R.id.event_info_loading_msg); 655 mTitle = (TextView) mView.findViewById(R.id.title); 656 mWhenDateTime = (TextView) mView.findViewById(R.id.when_datetime); 657 mWhere = (TextView) mView.findViewById(R.id.where); 658 mDesc = (ExpandableTextView) mView.findViewById(R.id.description); 659 mHeadlines = mView.findViewById(R.id.event_info_headline); 660 mLongAttendees = (AttendeesView)mView.findViewById(R.id.long_attendee_list); 661 mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config); 662 663 if (mUri == null) { 664 // restore event ID from bundle 665 mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID); 666 mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 667 mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS); 668 mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS); 669 } 670 671 mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0, 1); 672 mAnimateAlpha.setDuration(FADE_IN_TIME); 673 mAnimateAlpha.addListener(new AnimatorListenerAdapter() { 674 int defLayerType; 675 676 @Override 677 public void onAnimationStart(Animator animation) { 678 // Use hardware layer for better performance during animation 679 defLayerType = mScrollView.getLayerType(); 680 mScrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 681 // Ensure that the loading message is gone before showing the 682 // event info 683 mLoadingMsgView.removeCallbacks(mLoadingMsgAlphaUpdater); 684 mLoadingMsgView.setVisibility(View.GONE); 685 } 686 687 @Override 688 public void onAnimationCancel(Animator animation) { 689 mScrollView.setLayerType(defLayerType, null); 690 } 691 692 @Override 693 public void onAnimationEnd(Animator animation) { 694 mScrollView.setLayerType(defLayerType, null); 695 // Do not cross fade after the first time 696 mNoCrossFade = true; 697 } 698 }); 699 700 mLoadingMsgView.setAlpha(0); 701 mScrollView.setAlpha(0); 702 mLoadingMsgView.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY); 703 704 // start loading the data 705 706 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 707 null, null, null); 708 709 Button b = (Button) mView.findViewById(R.id.delete); 710 b.setOnClickListener(new OnClickListener() { 711 @Override 712 public void onClick(View v) { 713 if (!mCanModifyCalendar) { 714 return; 715 } 716 mDeleteHelper = new DeleteEventHelper( 717 mContext, mActivity, 718 !mIsDialog && !mIsTabletConfig /* exitWhenDone */); 719 mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this); 720 mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener()); 721 mDeleteDialogVisible = true; 722 mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); 723 }}); 724 725 // Hide Edit/Delete buttons if in full screen mode on a phone 726 if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) { 727 mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE); 728 } 729 730 // Create a listener for the email guests button 731 View emailAttendeesButton = mView.findViewById(R.id.email_attendees_button); 732 if (emailAttendeesButton != null) { 733 emailAttendeesButton.setOnClickListener(new View.OnClickListener() { 734 @Override 735 public void onClick(View v) { 736 emailAttendees(); 737 } 738 }); 739 } 740 741 // Create a listener for the add reminder button 742 View reminderAddButton = mView.findViewById(R.id.reminder_add); 743 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { 744 @Override 745 public void onClick(View v) { 746 addReminder(); 747 mUserModifiedReminders = true; 748 } 749 }; 750 reminderAddButton.setOnClickListener(addReminderOnClickListener); 751 752 // Set reminders variables 753 754 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity); 755 String defaultReminderString = prefs.getString( 756 GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING); 757 mDefaultReminderMinutes = Integer.parseInt(defaultReminderString); 758 prepareReminders(); 759 760 return mView; 761 } 762 763 private final Runnable onDeleteRunnable = new Runnable() { 764 @Override 765 public void run() { 766 if (EventInfoFragment.this.mIsPaused) { 767 mDismissOnResume = true; 768 return; 769 } 770 if (EventInfoFragment.this.isVisible()) { 771 EventInfoFragment.this.dismiss(); 772 } 773 } 774 }; 775 776 private void updateTitle() { 777 Resources res = getActivity().getResources(); 778 if (mCanModifyCalendar && !mIsOrganizer) { 779 getActivity().setTitle(res.getString(R.string.event_info_title_invite)); 780 } else { 781 getActivity().setTitle(res.getString(R.string.event_info_title)); 782 } 783 } 784 785 /** 786 * Initializes the event cursor, which is expected to point to the first 787 * (and only) result from a query. 788 * @return true if the cursor is empty. 789 */ 790 private boolean initEventCursor() { 791 if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) { 792 return true; 793 } 794 mEventCursor.moveToFirst(); 795 mEventId = mEventCursor.getInt(EVENT_INDEX_ID); 796 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 797 mIsRepeating = !TextUtils.isEmpty(rRule); 798 mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)?true:false; 799 mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS); 800 mCalendarAllowedReminders = mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS); 801 return false; 802 } 803 804 @SuppressWarnings("fallthrough") 805 private void initAttendeesCursor(View view) { 806 mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE; 807 mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 808 mNumOfAttendees = 0; 809 if (mAttendeesCursor != null) { 810 mNumOfAttendees = mAttendeesCursor.getCount(); 811 if (mAttendeesCursor.moveToFirst()) { 812 mAcceptedAttendees.clear(); 813 mDeclinedAttendees.clear(); 814 mTentativeAttendees.clear(); 815 mNoResponseAttendees.clear(); 816 817 do { 818 int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 819 String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME); 820 String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL); 821 822 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE && 823 mCalendarOwnerAccount.equalsIgnoreCase(email)) { 824 mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID); 825 mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 826 } else { 827 // Don't show your own status in the list because: 828 // 1) it doesn't make sense for event without other guests. 829 // 2) there's a spinner for that for events with guests. 830 switch(status) { 831 case Attendees.ATTENDEE_STATUS_ACCEPTED: 832 mAcceptedAttendees.add(new Attendee(name, email, 833 Attendees.ATTENDEE_STATUS_ACCEPTED)); 834 break; 835 case Attendees.ATTENDEE_STATUS_DECLINED: 836 mDeclinedAttendees.add(new Attendee(name, email, 837 Attendees.ATTENDEE_STATUS_DECLINED)); 838 break; 839 case Attendees.ATTENDEE_STATUS_TENTATIVE: 840 mTentativeAttendees.add(new Attendee(name, email, 841 Attendees.ATTENDEE_STATUS_TENTATIVE)); 842 break; 843 default: 844 mNoResponseAttendees.add(new Attendee(name, email, 845 Attendees.ATTENDEE_STATUS_NONE)); 846 } 847 } 848 } while (mAttendeesCursor.moveToNext()); 849 mAttendeesCursor.moveToFirst(); 850 851 updateAttendees(view); 852 } 853 } 854 } 855 856 @Override 857 public void onSaveInstanceState(Bundle outState) { 858 super.onSaveInstanceState(outState); 859 outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId); 860 outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis); 861 outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis); 862 outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog); 863 outState.putInt(BUNDLE_KEY_WINDOW_STYLE, mWindowStyle); 864 outState.putBoolean(BUNDLE_KEY_DELETE_DIALOG_VISIBLE, mDeleteDialogVisible); 865 outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, mAttendeeResponseFromIntent); 866 } 867 868 869 @Override 870 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 871 super.onCreateOptionsMenu(menu, inflater); 872 // Show edit/delete buttons only in non-dialog configuration 873 if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) { 874 inflater.inflate(R.menu.event_info_title_bar, menu); 875 mMenu = menu; 876 updateMenu(); 877 } 878 } 879 880 @Override 881 public boolean onOptionsItemSelected(MenuItem item) { 882 883 // If we're a dialog we don't want to handle menu buttons 884 if (mIsDialog) { 885 return false; 886 } 887 // Handles option menu selections: 888 // Home button - close event info activity and start the main calendar 889 // one 890 // Edit button - start the event edit activity and close the info 891 // activity 892 // Delete button - start a delete query that calls a runnable that close 893 // the info activity 894 895 switch (item.getItemId()) { 896 case android.R.id.home: 897 Utils.returnToCalendarHome(mContext); 898 mActivity.finish(); 899 return true; 900 case R.id.info_action_edit: 901 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 902 Intent intent = new Intent(Intent.ACTION_EDIT, uri); 903 intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis); 904 intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis); 905 intent.putExtra(EXTRA_EVENT_ALL_DAY, mAllDay); 906 intent.setClass(mActivity, EditEventActivity.class); 907 intent.putExtra(EVENT_EDIT_ON_LAUNCH, true); 908 startActivity(intent); 909 mActivity.finish(); 910 break; 911 case R.id.info_action_delete: 912 mDeleteHelper = 913 new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */); 914 mDeleteHelper.setDeleteNotificationListener(EventInfoFragment.this); 915 mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener()); 916 mDeleteDialogVisible = true; 917 mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); 918 break; 919 default: 920 break; 921 } 922 return super.onOptionsItemSelected(item); 923 } 924 925 @Override 926 public void onDestroyView() { 927 928 if (!mEventDeletionStarted) { 929 boolean responseSaved = saveResponse(); 930 if (saveReminders() || responseSaved) { 931 Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show(); 932 } 933 } 934 super.onDestroyView(); 935 } 936 937 @Override 938 public void onDestroy() { 939 if (mEventCursor != null) { 940 mEventCursor.close(); 941 } 942 if (mCalendarsCursor != null) { 943 mCalendarsCursor.close(); 944 } 945 if (mAttendeesCursor != null) { 946 mAttendeesCursor.close(); 947 } 948 super.onDestroy(); 949 } 950 951 /** 952 * Asynchronously saves the response to an invitation if the user changed 953 * the response. Returns true if the database will be updated. 954 * 955 * @return true if the database will be changed 956 */ 957 private boolean saveResponse() { 958 if (mAttendeesCursor == null || mEventCursor == null) { 959 return false; 960 } 961 962 RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value); 963 int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId()); 964 if (status == Attendees.ATTENDEE_STATUS_NONE) { 965 return false; 966 } 967 968 // If the status has not changed, then don't update the database 969 if (status == mOriginalAttendeeResponse) { 970 return false; 971 } 972 973 // If we never got an owner attendee id we can't set the status 974 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) { 975 return false; 976 } 977 978 if (!mIsRepeating) { 979 // This is a non-repeating event 980 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 981 return true; 982 } 983 984 // This is a repeating event 985 int whichEvents = mEditResponseHelper.getWhichEvents(); 986 switch (whichEvents) { 987 case -1: 988 return false; 989 case UPDATE_SINGLE: 990 createExceptionResponse(mEventId, status); 991 return true; 992 case UPDATE_ALL: 993 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 994 return true; 995 default: 996 Log.e(TAG, "Unexpected choice for updating invitation response"); 997 break; 998 } 999 return false; 1000 } 1001 1002 private void updateResponse(long eventId, long attendeeId, int status) { 1003 // Update the attendee status in the attendees table. the provider 1004 // takes care of updating the self attendance status. 1005 ContentValues values = new ContentValues(); 1006 1007 if (!TextUtils.isEmpty(mCalendarOwnerAccount)) { 1008 values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount); 1009 } 1010 values.put(Attendees.ATTENDEE_STATUS, status); 1011 values.put(Attendees.EVENT_ID, eventId); 1012 1013 Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId); 1014 1015 mHandler.startUpdate(mHandler.getNextToken(), null, uri, values, 1016 null, null, Utils.UNDO_DELAY); 1017 } 1018 1019 /** 1020 * Creates an exception to a recurring event. The only change we're making is to the 1021 * "self attendee status" value. The provider will take care of updating the corresponding 1022 * Attendees.attendeeStatus entry. 1023 * 1024 * @param eventId The recurring event. 1025 * @param status The new value for selfAttendeeStatus. 1026 */ 1027 private void createExceptionResponse(long eventId, int status) { 1028 ContentValues values = new ContentValues(); 1029 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis); 1030 values.put(Events.SELF_ATTENDEE_STATUS, status); 1031 values.put(Events.STATUS, Events.STATUS_CONFIRMED); 1032 1033 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 1034 Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, 1035 String.valueOf(eventId)); 1036 ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build()); 1037 1038 mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops, 1039 Utils.UNDO_DELAY); 1040 } 1041 1042 public static int getResponseFromButtonId(int buttonId) { 1043 int response; 1044 switch (buttonId) { 1045 case R.id.response_yes: 1046 response = Attendees.ATTENDEE_STATUS_ACCEPTED; 1047 break; 1048 case R.id.response_maybe: 1049 response = Attendees.ATTENDEE_STATUS_TENTATIVE; 1050 break; 1051 case R.id.response_no: 1052 response = Attendees.ATTENDEE_STATUS_DECLINED; 1053 break; 1054 default: 1055 response = Attendees.ATTENDEE_STATUS_NONE; 1056 } 1057 return response; 1058 } 1059 1060 public static int findButtonIdForResponse(int response) { 1061 int buttonId; 1062 switch (response) { 1063 case Attendees.ATTENDEE_STATUS_ACCEPTED: 1064 buttonId = R.id.response_yes; 1065 break; 1066 case Attendees.ATTENDEE_STATUS_TENTATIVE: 1067 buttonId = R.id.response_maybe; 1068 break; 1069 case Attendees.ATTENDEE_STATUS_DECLINED: 1070 buttonId = R.id.response_no; 1071 break; 1072 default: 1073 buttonId = -1; 1074 } 1075 return buttonId; 1076 } 1077 1078 private void doEdit() { 1079 Context c = getActivity(); 1080 // This ensures that we aren't in the process of closing and have been 1081 // unattached already 1082 if (c != null) { 1083 CalendarController.getInstance(c).sendEventRelatedEvent( 1084 this, EventType.EDIT_EVENT, mEventId, mStartMillis, mEndMillis, 0 1085 , 0, -1); 1086 } 1087 } 1088 1089 private void updateEvent(View view) { 1090 if (mEventCursor == null || view == null) { 1091 return; 1092 } 1093 1094 String eventName = mEventCursor.getString(EVENT_INDEX_TITLE); 1095 if (eventName == null || eventName.length() == 0) { 1096 eventName = getActivity().getString(R.string.no_title_label); 1097 } 1098 1099 mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 1100 String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION); 1101 String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION); 1102 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 1103 String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); 1104 1105 mColor = Utils.getDisplayColorFromColor(mEventCursor.getInt(EVENT_INDEX_COLOR)); 1106 mHeadlines.setBackgroundColor(mColor); 1107 1108 // What 1109 if (eventName != null) { 1110 setTextCommon(view, R.id.title, eventName); 1111 } 1112 1113 // When 1114 // Set the date and repeats (if any) 1115 String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater); 1116 Activity context = getActivity(); 1117 Resources resources = context.getResources(); 1118 String displayedDatetime = Utils.getDisplayedDatetime(mStartMillis, mEndMillis, 1119 System.currentTimeMillis(), localTimezone, mAllDay, context); 1120 String displayedTimezone = Utils.getDisplayedTimezone(mStartMillis, localTimezone, 1121 eventTimezone); 1122 int timezoneIndex = displayedDatetime.length(); 1123 if (displayedTimezone != null) { 1124 displayedDatetime += " " + displayedTimezone; 1125 } 1126 // Display the datetime. Make the timezone (if any) transparent. 1127 if (displayedTimezone == null) { 1128 setTextCommon(view, R.id.when_datetime, displayedDatetime); 1129 } else { 1130 SpannableStringBuilder sb = new SpannableStringBuilder(displayedDatetime); 1131 ForegroundColorSpan transparentColorSpan = new ForegroundColorSpan( 1132 resources.getColor(R.color.event_info_headline_transparent_color)); 1133 sb.setSpan(transparentColorSpan, timezoneIndex, displayedDatetime.length(), 1134 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 1135 setTextCommon(view, R.id.when_datetime, sb); 1136 } 1137 1138 // Display the repeat string (if any) 1139 String repeatString = null; 1140 if (!TextUtils.isEmpty(rRule)) { 1141 EventRecurrence eventRecurrence = new EventRecurrence(); 1142 eventRecurrence.parse(rRule); 1143 Time date = new Time(localTimezone); 1144 date.set(mStartMillis); 1145 if (mAllDay) { 1146 date.timezone = Time.TIMEZONE_UTC; 1147 } 1148 eventRecurrence.setStartDate(date); 1149 repeatString = EventRecurrenceFormatter.getRepeatString(resources, eventRecurrence); 1150 } 1151 if (repeatString == null) { 1152 view.findViewById(R.id.when_repeat).setVisibility(View.GONE); 1153 } else { 1154 setTextCommon(view, R.id.when_repeat, repeatString); 1155 } 1156 1157 // Organizer view is setup in the updateCalendar method 1158 1159 1160 // Where 1161 if (location == null || location.trim().length() == 0) { 1162 setVisibilityCommon(view, R.id.where, View.GONE); 1163 } else { 1164 final TextView textView = mWhere; 1165 if (textView != null) { 1166 textView.setAutoLinkMask(0); 1167 textView.setText(location.trim()); 1168 try { 1169 linkifyTextView(textView); 1170 } catch (Exception ex) { 1171 // unexpected 1172 Log.e(TAG, "Linkification failed", ex); 1173 } 1174 1175 textView.setOnTouchListener(new OnTouchListener() { 1176 @Override 1177 public boolean onTouch(View v, MotionEvent event) { 1178 try { 1179 return v.onTouchEvent(event); 1180 } catch (ActivityNotFoundException e) { 1181 // ignore 1182 return true; 1183 } 1184 } 1185 }); 1186 } 1187 } 1188 1189 // Description 1190 if (description != null && description.length() != 0) { 1191 mDesc.setText(description); 1192 } 1193 1194 } 1195 1196 /** 1197 * Finds North American Numbering Plan (NANP) phone numbers in the input text. 1198 * 1199 * @param text The text to scan. 1200 * @return A list of [start, end) pairs indicating the positions of phone numbers in the input. 1201 */ 1202 // @VisibleForTesting 1203 static int[] findNanpPhoneNumbers(CharSequence text) { 1204 ArrayList<Integer> list = new ArrayList<Integer>(); 1205 1206 int startPos = 0; 1207 int endPos = text.length() - NANP_MIN_DIGITS + 1; 1208 if (endPos < 0) { 1209 return new int[] {}; 1210 } 1211 1212 /* 1213 * We can't just strip the whitespace out and crunch it down, because the whitespace 1214 * is significant. March through, trying to figure out where numbers start and end. 1215 */ 1216 while (startPos < endPos) { 1217 // skip whitespace 1218 while (Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) { 1219 startPos++; 1220 } 1221 if (startPos == endPos) { 1222 break; 1223 } 1224 1225 // check for a match at this position 1226 int matchEnd = findNanpMatchEnd(text, startPos); 1227 if (matchEnd > startPos) { 1228 list.add(startPos); 1229 list.add(matchEnd); 1230 startPos = matchEnd; // skip past match 1231 } else { 1232 // skip to next whitespace char 1233 while (!Character.isWhitespace(text.charAt(startPos)) && startPos < endPos) { 1234 startPos++; 1235 } 1236 } 1237 } 1238 1239 int[] result = new int[list.size()]; 1240 for (int i = list.size() - 1; i >= 0; i--) { 1241 result[i] = list.get(i); 1242 } 1243 return result; 1244 } 1245 1246 /** 1247 * Checks to see if there is a valid phone number in the input, starting at the specified 1248 * offset. If so, the index of the last character + 1 is returned. The input is assumed 1249 * to begin with a non-whitespace character. 1250 * 1251 * @return Exclusive end position, or -1 if not a match. 1252 */ 1253 private static int findNanpMatchEnd(CharSequence text, int startPos) { 1254 /* 1255 * A few interesting cases: 1256 * 94043 # too short, ignore 1257 * 123456789012 # too long, ignore 1258 * +1 (650) 555-1212 # 11 digits, spaces 1259 * (650) 555-1212, (650) 555-1213 # two numbers, return first 1260 * 1-650-555-1212 # 11 digits with leading '1' 1261 * *#650.555.1212#*! # 10 digits, include #*, ignore trailing '!' 1262 * 555.1212 # 7 digits 1263 * 1264 * For the most part we want to break on whitespace, but it's common to leave a space 1265 * between the initial '1' and/or after the area code. 1266 */ 1267 1268 int endPos = text.length(); 1269 int curPos = startPos; 1270 int foundDigits = 0; 1271 char firstDigit = 'x'; 1272 1273 while (curPos <= endPos) { 1274 char ch; 1275 if (curPos < endPos) { 1276 ch = text.charAt(curPos); 1277 } else { 1278 ch = 27; // fake invalid symbol at end to trigger loop break 1279 } 1280 1281 if (Character.isDigit(ch)) { 1282 if (foundDigits == 0) { 1283 firstDigit = ch; 1284 } 1285 foundDigits++; 1286 if (foundDigits > NANP_MAX_DIGITS) { 1287 // too many digits, stop early 1288 return -1; 1289 } 1290 } else if (Character.isWhitespace(ch)) { 1291 if (!( (firstDigit == '1' && (foundDigits == 1 || foundDigits == 4)) || 1292 (foundDigits == 3)) ) { 1293 break; 1294 } 1295 } else if (NANP_ALLOWED_SYMBOLS.indexOf(ch) == -1) { 1296 break; 1297 } 1298 // else it's an allowed symbol 1299 1300 curPos++; 1301 } 1302 1303 if ((firstDigit != '1' && (foundDigits == 7 || foundDigits == 10)) || 1304 (firstDigit == '1' && foundDigits == 11)) { 1305 // match 1306 return curPos; 1307 } 1308 1309 return -1; 1310 } 1311 1312 /** 1313 * Replaces stretches of text that look like addresses and phone numbers with clickable 1314 * links. 1315 * <p> 1316 * This is really just an enhanced version of Linkify.addLinks(). 1317 */ 1318 private static void linkifyTextView(TextView textView) { 1319 /* 1320 * If the text includes a street address like "1600 Amphitheater Parkway, 94043", 1321 * the current Linkify code will identify "94043" as a phone number and invite 1322 * you to dial it (and not provide a map link for the address). We want to 1323 * have better recognition of phone numbers without losing any of the existing 1324 * annotations. 1325 * 1326 * Ideally this would be addressed by improving Linkify. For now we manage it as 1327 * a second pass over the text. 1328 * 1329 * URIs and e-mail addresses are pretty easy to pick out of text. Phone numbers 1330 * are a bit tricky because they have radically different formats in different 1331 * countries, in terms of both the digits and the way in which they are commonly 1332 * written or presented (e.g. the punctuation and spaces in "(650) 555-1212"). 1333 * The expected format of a street address is defined in WebView.findAddress(). It's 1334 * pretty narrowly defined, so it won't often match. 1335 * 1336 * The RFC 3966 specification defines the format of a "tel:" URI. 1337 */ 1338 1339 /* 1340 * If we're in the US, handle this specially. Otherwise, punt to Linkify. 1341 */ 1342 String defaultPhoneRegion = System.getProperty("user.region", "US"); 1343 if (!defaultPhoneRegion.equals("US")) { 1344 Linkify.addLinks(textView, Linkify.ALL); 1345 return; 1346 } 1347 1348 /* 1349 * Start by letting Linkify find anything that isn't a phone number. We have to let it 1350 * run first because every invocation removes all previous URLSpan annotations. 1351 * 1352 * Ideally we'd use the external/libphonenumber routines, but those aren't available 1353 * to unbundled applications. 1354 */ 1355 boolean linkifyFoundLinks = Linkify.addLinks(textView, 1356 Linkify.ALL & ~(Linkify.PHONE_NUMBERS)); 1357 1358 /* 1359 * Search for phone numbers. 1360 * 1361 * Some URIs contain strings of digits that look like phone numbers. If both the URI 1362 * scanner and the phone number scanner find them, we want the URI link to win. Since 1363 * the URI scanner runs first, we just need to avoid creating overlapping spans. 1364 */ 1365 CharSequence text = textView.getText(); 1366 int[] phoneSequences = findNanpPhoneNumbers(text); 1367 1368 /* 1369 * If the contents of the TextView are already Spannable (which will be the case if 1370 * Linkify found stuff, but might not be otherwise), we can just add annotations 1371 * to what's there. If it's not, and we find phone numbers, we need to convert it to 1372 * a Spannable form. (This mimics the behavior of Linkable.addLinks().) 1373 */ 1374 Spannable spanText; 1375 if (text instanceof SpannableString) { 1376 spanText = (SpannableString) text; 1377 } else { 1378 spanText = SpannableString.valueOf(text); 1379 } 1380 1381 /* 1382 * Get a list of any spans created by Linkify, for the overlapping span check. 1383 */ 1384 URLSpan[] existingSpans = spanText.getSpans(0, spanText.length(), URLSpan.class); 1385 1386 /* 1387 * Insert spans for the numbers we found. We generate "tel:" URIs. 1388 */ 1389 int phoneCount = 0; 1390 for (int match = 0; match < phoneSequences.length / 2; match++) { 1391 int start = phoneSequences[match*2]; 1392 int end = phoneSequences[match*2 + 1]; 1393 1394 if (spanWillOverlap(spanText, existingSpans, start, end)) { 1395 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1396 CharSequence seq = text.subSequence(start, end); 1397 Log.v(TAG, "Not linkifying " + seq + " as phone number due to overlap"); 1398 } 1399 continue; 1400 } 1401 1402 /* 1403 * The Linkify code takes the matching span and strips out everything that isn't a 1404 * digit or '+' sign. We do the same here. Extension numbers will get appended 1405 * without a separator, but the dialer wasn't doing anything useful with ";ext=" 1406 * anyway. 1407 */ 1408 1409 //String dialStr = phoneUtil.format(match.number(), 1410 // PhoneNumberUtil.PhoneNumberFormat.RFC3966); 1411 StringBuilder dialBuilder = new StringBuilder(); 1412 for (int i = start; i < end; i++) { 1413 char ch = spanText.charAt(i); 1414 if (ch == '+' || Character.isDigit(ch)) { 1415 dialBuilder.append(ch); 1416 } 1417 } 1418 URLSpan span = new URLSpan("tel:" + dialBuilder.toString()); 1419 1420 spanText.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1421 phoneCount++; 1422 } 1423 1424 if (phoneCount != 0) { 1425 // If we had to "upgrade" to Spannable, store the object into the TextView. 1426 if (spanText != text) { 1427 textView.setText(spanText); 1428 } 1429 1430 // Linkify.addLinks() sets the TextView movement method if it finds any links. We 1431 // want to do the same here. (This is cloned from Linkify.addLinkMovementMethod().) 1432 MovementMethod mm = textView.getMovementMethod(); 1433 1434 if ((mm == null) || !(mm instanceof LinkMovementMethod)) { 1435 if (textView.getLinksClickable()) { 1436 textView.setMovementMethod(LinkMovementMethod.getInstance()); 1437 } 1438 } 1439 } 1440 1441 if (!linkifyFoundLinks && phoneCount == 0) { 1442 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1443 Log.v(TAG, "No linkification matches, using geo default"); 1444 } 1445 Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); 1446 } 1447 } 1448 1449 /** 1450 * Determines whether a new span at [start,end) will overlap with any existing span. 1451 */ 1452 private static boolean spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start, 1453 int end) { 1454 if (start == end) { 1455 // empty span, ignore 1456 return false; 1457 } 1458 for (URLSpan span : spanList) { 1459 int existingStart = spanText.getSpanStart(span); 1460 int existingEnd = spanText.getSpanEnd(span); 1461 if ((start >= existingStart && start < existingEnd) || 1462 end > existingStart && end <= existingEnd) { 1463 return true; 1464 } 1465 } 1466 1467 return false; 1468 } 1469 1470 private void sendAccessibilityEvent() { 1471 AccessibilityManager am = 1472 (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE); 1473 if (!am.isEnabled()) { 1474 return; 1475 } 1476 1477 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 1478 event.setClassName(getClass().getName()); 1479 event.setPackageName(getActivity().getPackageName()); 1480 List<CharSequence> text = event.getText(); 1481 1482 addFieldToAccessibilityEvent(text, mTitle, null); 1483 addFieldToAccessibilityEvent(text, mWhenDateTime, null); 1484 addFieldToAccessibilityEvent(text, mWhere, null); 1485 addFieldToAccessibilityEvent(text, null, mDesc); 1486 1487 RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value); 1488 if (response.getVisibility() == View.VISIBLE) { 1489 int id = response.getCheckedRadioButtonId(); 1490 if (id != View.NO_ID) { 1491 text.add(((TextView) getView().findViewById(R.id.response_label)).getText()); 1492 text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE)); 1493 } 1494 } 1495 1496 am.sendAccessibilityEvent(event); 1497 } 1498 1499 private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView tv, 1500 ExpandableTextView etv) { 1501 CharSequence cs; 1502 if (tv != null) { 1503 cs = tv.getText(); 1504 } else if (etv != null) { 1505 cs = etv.getText(); 1506 } else { 1507 return; 1508 } 1509 1510 if (!TextUtils.isEmpty(cs)) { 1511 cs = cs.toString().trim(); 1512 if (cs.length() > 0) { 1513 text.add(cs); 1514 text.add(PERIOD_SPACE); 1515 } 1516 } 1517 } 1518 1519 private void updateCalendar(View view) { 1520 mCalendarOwnerAccount = ""; 1521 if (mCalendarsCursor != null && mEventCursor != null) { 1522 mCalendarsCursor.moveToFirst(); 1523 String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 1524 mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount; 1525 mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0; 1526 mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME); 1527 1528 String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 1529 1530 // start duplicate calendars query 1531 mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI, 1532 CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE, 1533 new String[] {displayName}, null); 1534 1535 mEventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER); 1536 mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(mEventOrganizer); 1537 setTextCommon(view, R.id.organizer, mEventOrganizer); 1538 if (!mIsOrganizer) { 1539 setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); 1540 } else { 1541 setVisibilityCommon(view, R.id.organizer_container, View.GONE); 1542 } 1543 mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0; 1544 mCanModifyCalendar = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) 1545 >= Calendars.CAL_ACCESS_CONTRIBUTOR; 1546 // TODO add "|| guestCanModify" after b/1299071 is fixed 1547 mCanModifyEvent = mCanModifyCalendar && mIsOrganizer; 1548 mIsBusyFreeCalendar = 1549 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY; 1550 1551 if (!mIsBusyFreeCalendar) { 1552 Button b = (Button) mView.findViewById(R.id.edit); 1553 b.setEnabled(true); 1554 b.setOnClickListener(new OnClickListener() { 1555 @Override 1556 public void onClick(View v) { 1557 doEdit(); 1558 // For dialogs, just close the fragment 1559 // For full screen, close activity on phone, leave it for tablet 1560 if (mIsDialog) { 1561 EventInfoFragment.this.dismiss(); 1562 } 1563 else if (!mIsTabletConfig){ 1564 getActivity().finish(); 1565 } 1566 } 1567 }); 1568 } 1569 View button; 1570 if (mCanModifyCalendar) { 1571 button = mView.findViewById(R.id.delete); 1572 if (button != null) { 1573 button.setEnabled(true); 1574 button.setVisibility(View.VISIBLE); 1575 } 1576 } 1577 if (mCanModifyEvent) { 1578 button = mView.findViewById(R.id.edit); 1579 if (button != null) { 1580 button.setEnabled(true); 1581 button.setVisibility(View.VISIBLE); 1582 } 1583 } 1584 1585 if ((!mIsDialog && !mIsTabletConfig || 1586 mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) && mMenu != null) { 1587 mActivity.invalidateOptionsMenu(); 1588 } 1589 } else { 1590 setVisibilityCommon(view, R.id.calendar, View.GONE); 1591 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); 1592 } 1593 } 1594 1595 /** 1596 * 1597 */ 1598 private void updateMenu() { 1599 if (mMenu == null) { 1600 return; 1601 } 1602 MenuItem delete = mMenu.findItem(R.id.info_action_delete); 1603 MenuItem edit = mMenu.findItem(R.id.info_action_edit); 1604 if (delete != null) { 1605 delete.setVisible(mCanModifyCalendar); 1606 delete.setEnabled(mCanModifyCalendar); 1607 } 1608 if (edit != null) { 1609 edit.setVisible(mCanModifyEvent); 1610 edit.setEnabled(mCanModifyEvent); 1611 } 1612 } 1613 1614 private void updateAttendees(View view) { 1615 if (mAcceptedAttendees.size() + mDeclinedAttendees.size() + 1616 mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) { 1617 mLongAttendees.clearAttendees(); 1618 (mLongAttendees).addAttendees(mAcceptedAttendees); 1619 (mLongAttendees).addAttendees(mDeclinedAttendees); 1620 (mLongAttendees).addAttendees(mTentativeAttendees); 1621 (mLongAttendees).addAttendees(mNoResponseAttendees); 1622 mLongAttendees.setEnabled(false); 1623 mLongAttendees.setVisibility(View.VISIBLE); 1624 } else { 1625 mLongAttendees.setVisibility(View.GONE); 1626 } 1627 1628 updateEmailAttendees(); 1629 } 1630 1631 /** 1632 * Initializes the list of 'to' and 'cc' emails from the attendee list. 1633 */ 1634 private void updateEmailAttendees() { 1635 // The declined attendees will go in the 'cc' line, all others will go in the 'to' line. 1636 mToEmails = new ArrayList<String>(); 1637 for (Attendee attendee : mAcceptedAttendees) { 1638 addIfEmailable(mToEmails, attendee.mEmail); 1639 } 1640 for (Attendee attendee : mTentativeAttendees) { 1641 addIfEmailable(mToEmails, attendee.mEmail); 1642 } 1643 for (Attendee attendee : mNoResponseAttendees) { 1644 addIfEmailable(mToEmails, attendee.mEmail); 1645 } 1646 mCcEmails = new ArrayList<String>(); 1647 for (Attendee attendee : this.mDeclinedAttendees) { 1648 addIfEmailable(mCcEmails, attendee.mEmail); 1649 } 1650 1651 // The meeting organizer doesn't appear as an attendee sometimes (particularly 1652 // when viewing someone else's calendar), so add the organizer now. 1653 if (mEventOrganizer != null && !mToEmails.contains(mEventOrganizer) && 1654 !mCcEmails.contains(mEventOrganizer)) { 1655 addIfEmailable(mToEmails, mEventOrganizer); 1656 } 1657 1658 // The Email app behaves strangely when there is nothing in the 'mailto' part, 1659 // so move all the 'cc' emails to the 'to' list. Gmail works fine though. 1660 if (mToEmails.size() <= 0 && mCcEmails.size() > 0) { 1661 mToEmails.addAll(mCcEmails); 1662 mCcEmails.clear(); 1663 } 1664 1665 if (mToEmails.size() <= 0) { 1666 setVisibilityCommon(mView, R.id.email_attendees_container, View.GONE); 1667 } else { 1668 setVisibilityCommon(mView, R.id.email_attendees_container, View.VISIBLE); 1669 } 1670 } 1671 1672 public void initReminders(View view, Cursor cursor) { 1673 1674 // Add reminders 1675 mOriginalReminders.clear(); 1676 mUnsupportedReminders.clear(); 1677 while (cursor.moveToNext()) { 1678 int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES); 1679 int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD); 1680 1681 if (method != Reminders.METHOD_DEFAULT && !mReminderMethodValues.contains(method)) { 1682 // Stash unsupported reminder types separately so we don't alter 1683 // them in the UI 1684 mUnsupportedReminders.add(ReminderEntry.valueOf(minutes, method)); 1685 } else { 1686 mOriginalReminders.add(ReminderEntry.valueOf(minutes, method)); 1687 } 1688 } 1689 // Sort appropriately for display (by time, then type) 1690 Collections.sort(mOriginalReminders); 1691 1692 if (mUserModifiedReminders) { 1693 // If the user has changed the list of reminders don't change what's 1694 // shown. 1695 return; 1696 } 1697 1698 LinearLayout parent = (LinearLayout) mScrollView 1699 .findViewById(R.id.reminder_items_container); 1700 if (parent != null) { 1701 parent.removeAllViews(); 1702 } 1703 if (mReminderViews != null) { 1704 mReminderViews.clear(); 1705 } 1706 1707 if (mHasAlarm) { 1708 ArrayList<ReminderEntry> reminders = mOriginalReminders; 1709 // Insert any minute values that aren't represented in the minutes list. 1710 for (ReminderEntry re : reminders) { 1711 EventViewUtils.addMinutesToList( 1712 mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes()); 1713 } 1714 // Create a UI element for each reminder. We display all of the reminders we get 1715 // from the provider, even if the count exceeds the calendar maximum. (Also, for 1716 // a new event, we won't have a maxReminders value available.) 1717 for (ReminderEntry re : reminders) { 1718 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1719 mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues, 1720 mReminderMethodLabels, re, Integer.MAX_VALUE, mReminderChangeListener); 1721 } 1722 EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders); 1723 // TODO show unsupported reminder types in some fashion. 1724 } 1725 } 1726 1727 void updateResponse(View view) { 1728 // we only let the user accept/reject/etc. a meeting if: 1729 // a) you can edit the event's containing calendar AND 1730 // b) you're not the organizer and only attendee AND 1731 // c) organizerCanRespond is enabled for the calendar 1732 // (if the attendee data has been hidden, the visible number of attendees 1733 // will be 1 -- the calendar owner's). 1734 // (there are more cases involved to be 100% accurate, such as 1735 // paying attention to whether or not an attendee status was 1736 // included in the feed, but we're currently omitting those corner cases 1737 // for simplicity). 1738 1739 // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel. 1740 if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) || 1741 (mIsOrganizer && !mOwnerCanRespond)) { 1742 setVisibilityCommon(view, R.id.response_container, View.GONE); 1743 return; 1744 } 1745 1746 setVisibilityCommon(view, R.id.response_container, View.VISIBLE); 1747 1748 1749 int response; 1750 if (mUserSetResponse != CalendarController.ATTENDEE_NO_RESPONSE) { 1751 response = mUserSetResponse; 1752 } else if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) { 1753 response = mAttendeeResponseFromIntent; 1754 } else { 1755 response = mOriginalAttendeeResponse; 1756 } 1757 1758 int buttonToCheck = findButtonIdForResponse(response); 1759 RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value); 1760 radioGroup.check(buttonToCheck); // -1 clear all radio buttons 1761 radioGroup.setOnCheckedChangeListener(this); 1762 } 1763 1764 private void setTextCommon(View view, int id, CharSequence text) { 1765 TextView textView = (TextView) view.findViewById(id); 1766 if (textView == null) 1767 return; 1768 textView.setText(text); 1769 } 1770 1771 private void setVisibilityCommon(View view, int id, int visibility) { 1772 View v = view.findViewById(id); 1773 if (v != null) { 1774 v.setVisibility(visibility); 1775 } 1776 return; 1777 } 1778 1779 /** 1780 * Taken from com.google.android.gm.HtmlConversationActivity 1781 * 1782 * Send the intent that shows the Contact info corresponding to the email address. 1783 */ 1784 public void showContactInfo(Attendee attendee, Rect rect) { 1785 // First perform lookup query to find existing contact 1786 final ContentResolver resolver = getActivity().getContentResolver(); 1787 final String address = attendee.mEmail; 1788 final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI, 1789 Uri.encode(address)); 1790 final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri); 1791 1792 if (lookupUri != null) { 1793 // Found matching contact, trigger QuickContact 1794 QuickContact.showQuickContact(getActivity(), rect, lookupUri, 1795 QuickContact.MODE_MEDIUM, null); 1796 } else { 1797 // No matching contact, ask user to create one 1798 final Uri mailUri = Uri.fromParts("mailto", address, null); 1799 final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri); 1800 1801 // Pass along full E-mail string for possible create dialog 1802 Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null); 1803 intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString()); 1804 1805 // Only provide personal name hint if we have one 1806 final String senderPersonal = attendee.mName; 1807 if (!TextUtils.isEmpty(senderPersonal)) { 1808 intent.putExtra(Intents.Insert.NAME, senderPersonal); 1809 } 1810 1811 startActivity(intent); 1812 } 1813 } 1814 1815 @Override 1816 public void onPause() { 1817 mIsPaused = true; 1818 mHandler.removeCallbacks(onDeleteRunnable); 1819 super.onPause(); 1820 // Remove event deletion alert box since it is being rebuild in the OnResume 1821 // This is done to get the same behavior on OnResume since the AlertDialog is gone on 1822 // rotation but not if you press the HOME key 1823 if (mDeleteDialogVisible && mDeleteHelper != null) { 1824 mDeleteHelper.dismissAlertDialog(); 1825 mDeleteHelper = null; 1826 } 1827 } 1828 1829 @Override 1830 public void onResume() { 1831 super.onResume(); 1832 mIsPaused = false; 1833 if (mDismissOnResume) { 1834 mHandler.post(onDeleteRunnable); 1835 } 1836 // Display the "delete confirmation" dialog if needed 1837 if (mDeleteDialogVisible) { 1838 mDeleteHelper = new DeleteEventHelper( 1839 mContext, mActivity, 1840 !mIsDialog && !mIsTabletConfig /* exitWhenDone */); 1841 mDeleteHelper.setOnDismissListener(createDeleteOnDismissListener()); 1842 mDeleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); 1843 } 1844 } 1845 1846 @Override 1847 public void eventsChanged() { 1848 } 1849 1850 @Override 1851 public long getSupportedEventTypes() { 1852 return EventType.EVENTS_CHANGED; 1853 } 1854 1855 @Override 1856 public void handleEvent(EventInfo event) { 1857 if (event.eventType == EventType.EVENTS_CHANGED && mHandler != null) { 1858 // reload the data 1859 reloadEvents(); 1860 } 1861 } 1862 1863 public void reloadEvents() { 1864 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 1865 null, null, null); 1866 } 1867 1868 @Override 1869 public void onClick(View view) { 1870 1871 // This must be a click on one of the "remove reminder" buttons 1872 LinearLayout reminderItem = (LinearLayout) view.getParent(); 1873 LinearLayout parent = (LinearLayout) reminderItem.getParent(); 1874 parent.removeView(reminderItem); 1875 mReminderViews.remove(reminderItem); 1876 mUserModifiedReminders = true; 1877 EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders); 1878 } 1879 1880 1881 /** 1882 * Add a new reminder when the user hits the "add reminder" button. We use the default 1883 * reminder time and method. 1884 */ 1885 private void addReminder() { 1886 // TODO: when adding a new reminder, make it different from the 1887 // last one in the list (if any). 1888 if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) { 1889 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1890 mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues, 1891 mReminderMethodLabels, 1892 ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), mMaxReminders, 1893 mReminderChangeListener); 1894 } else { 1895 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1896 mReminderMinuteValues, mReminderMinuteLabels, mReminderMethodValues, 1897 mReminderMethodLabels, ReminderEntry.valueOf(mDefaultReminderMinutes), 1898 mMaxReminders, mReminderChangeListener); 1899 } 1900 1901 EventViewUtils.updateAddReminderButton(mView, mReminderViews, mMaxReminders); 1902 } 1903 1904 synchronized private void prepareReminders() { 1905 // Nothing to do if we've already built these lists _and_ we aren't 1906 // removing not allowed methods 1907 if (mReminderMinuteValues != null && mReminderMinuteLabels != null 1908 && mReminderMethodValues != null && mReminderMethodLabels != null 1909 && mCalendarAllowedReminders == null) { 1910 return; 1911 } 1912 // Load the labels and corresponding numeric values for the minutes and methods lists 1913 // from the assets. If we're switching calendars, we need to clear and re-populate the 1914 // lists (which may have elements added and removed based on calendar properties). This 1915 // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a 1916 // new event that aren't in the default set. 1917 Resources r = mActivity.getResources(); 1918 mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values); 1919 mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels); 1920 mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values); 1921 mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels); 1922 1923 // Remove any reminder methods that aren't allowed for this calendar. If this is 1924 // a new event, mCalendarAllowedReminders may not be set the first time we're called. 1925 if (mCalendarAllowedReminders != null) { 1926 EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels, 1927 mCalendarAllowedReminders); 1928 } 1929 if (mView != null) { 1930 mView.invalidate(); 1931 } 1932 } 1933 1934 1935 private boolean saveReminders() { 1936 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3); 1937 1938 // Read reminders from UI 1939 mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews, 1940 mReminderMinuteValues, mReminderMethodValues); 1941 mOriginalReminders.addAll(mUnsupportedReminders); 1942 Collections.sort(mOriginalReminders); 1943 mReminders.addAll(mUnsupportedReminders); 1944 Collections.sort(mReminders); 1945 1946 // Check if there are any changes in the reminder 1947 boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders, 1948 mOriginalReminders, false /* no force save */); 1949 1950 if (!changed) { 1951 return false; 1952 } 1953 1954 // save new reminders 1955 AsyncQueryService service = new AsyncQueryService(getActivity()); 1956 service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0); 1957 // Update the "hasAlarm" field for the event 1958 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 1959 int len = mReminders.size(); 1960 boolean hasAlarm = len > 0; 1961 if (hasAlarm != mHasAlarm) { 1962 ContentValues values = new ContentValues(); 1963 values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0); 1964 service.startUpdate(0, null, uri, values, null, null, 0); 1965 } 1966 return true; 1967 } 1968 1969 /** 1970 * Adds the attendee's email to the list if: 1971 * (1) the attendee is not a resource like a conference room or another calendar. 1972 * Catch most of these by filtering out suffix calendar.google.com. 1973 * (2) the attendee is not the viewer, to prevent mailing himself. 1974 */ 1975 private void addIfEmailable(ArrayList<String> emailList, String email) { 1976 if (email != null && !email.equals(mSyncAccountName) && 1977 !email.endsWith("calendar.google.com")) { 1978 emailList.add(email); 1979 } 1980 } 1981 1982 /** 1983 * Email all the attendees of the event, except for the viewer (so as to not email 1984 * himself) and resources like conference rooms. 1985 */ 1986 private void emailAttendees() { 1987 String eventTitle = (mTitle == null || mTitle.getText() == null) ? null : 1988 mTitle.getText().toString(); 1989 Intent emailIntent = Utils.createEmailAttendeesIntent(getActivity().getResources(), 1990 eventTitle, mToEmails, mCcEmails, mCalendarOwnerAccount); 1991 startActivity(emailIntent); 1992 } 1993 1994 /** 1995 * Loads an integer array asset into a list. 1996 */ 1997 private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) { 1998 int[] vals = r.getIntArray(resNum); 1999 int size = vals.length; 2000 ArrayList<Integer> list = new ArrayList<Integer>(size); 2001 2002 for (int i = 0; i < size; i++) { 2003 list.add(vals[i]); 2004 } 2005 2006 return list; 2007 } 2008 /** 2009 * Loads a String array asset into a list. 2010 */ 2011 private static ArrayList<String> loadStringArray(Resources r, int resNum) { 2012 String[] labels = r.getStringArray(resNum); 2013 ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels)); 2014 return list; 2015 } 2016 2017 public void onDeleteStarted() { 2018 mEventDeletionStarted = true; 2019 } 2020 2021 private Dialog.OnDismissListener createDeleteOnDismissListener() { 2022 return new Dialog.OnDismissListener() { 2023 @Override 2024 public void onDismiss(DialogInterface dialog) { 2025 // Since OnPause will force the dialog to dismiss , do 2026 // not change the dialog status 2027 if (!mIsPaused) { 2028 mDeleteDialogVisible = false; 2029 } 2030 } 2031 }; 2032 } 2033 2034 public long getEventId() { 2035 return mEventId; 2036 } 2037 2038 public long getStartMillis() { 2039 return mStartMillis; 2040 } 2041 public long getEndMillis() { 2042 return mEndMillis; 2043 } 2044 2045 2046 2047} 2048