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