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