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 17 package com.android.calendar; 18 19 import static android.provider.CalendarContract.EXTRA_EVENT_ALL_DAY; 20 import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; 21 import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; 22 import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.ObjectAnimator; 27 import android.app.Activity; 28 import android.app.Dialog; 29 import android.app.DialogFragment; 30 import android.app.Service; 31 import android.content.ActivityNotFoundException; 32 import android.content.ContentProviderOperation; 33 import android.content.ContentResolver; 34 import android.content.ContentUris; 35 import android.content.ContentValues; 36 import android.content.Context; 37 import android.content.DialogInterface; 38 import android.content.Intent; 39 import android.content.SharedPreferences; 40 import android.content.pm.ApplicationInfo; 41 import android.content.pm.PackageManager; 42 import android.content.pm.PackageManager.NameNotFoundException; 43 import android.content.res.Resources; 44 import android.database.Cursor; 45 import android.graphics.Rect; 46 import android.graphics.drawable.Drawable; 47 import android.net.Uri; 48 import android.os.Bundle; 49 import android.provider.CalendarContract; 50 import android.provider.CalendarContract.Attendees; 51 import android.provider.CalendarContract.Calendars; 52 import android.provider.CalendarContract.Events; 53 import android.provider.CalendarContract.Reminders; 54 import android.provider.ContactsContract; 55 import android.provider.ContactsContract.CommonDataKinds; 56 import android.provider.ContactsContract.Intents; 57 import android.provider.ContactsContract.QuickContact; 58 import android.text.Spannable; 59 import android.text.SpannableString; 60 import android.text.SpannableStringBuilder; 61 import android.text.Spanned; 62 import android.text.TextUtils; 63 import android.text.format.Time; 64 import android.text.method.LinkMovementMethod; 65 import android.text.method.MovementMethod; 66 import android.text.style.ForegroundColorSpan; 67 import android.text.style.URLSpan; 68 import android.text.util.Linkify; 69 import android.text.util.Rfc822Token; 70 import android.util.Log; 71 import android.view.Gravity; 72 import android.view.LayoutInflater; 73 import android.view.Menu; 74 import android.view.MenuInflater; 75 import android.view.MenuItem; 76 import android.view.MotionEvent; 77 import android.view.View; 78 import android.view.View.OnClickListener; 79 import android.view.View.OnTouchListener; 80 import android.view.ViewGroup; 81 import android.view.Window; 82 import android.view.WindowManager; 83 import android.view.accessibility.AccessibilityEvent; 84 import android.view.accessibility.AccessibilityManager; 85 import android.widget.AdapterView; 86 import android.widget.AdapterView.OnItemSelectedListener; 87 import android.widget.Button; 88 import android.widget.LinearLayout; 89 import android.widget.RadioButton; 90 import android.widget.RadioGroup; 91 import android.widget.RadioGroup.OnCheckedChangeListener; 92 import android.widget.ScrollView; 93 import android.widget.TextView; 94 import android.widget.Toast; 95 96 import com.android.calendar.CalendarController.EventInfo; 97 import com.android.calendar.CalendarController.EventType; 98 import com.android.calendar.CalendarEventModel.Attendee; 99 import com.android.calendar.CalendarEventModel.ReminderEntry; 100 import com.android.calendar.alerts.QuickResponseActivity; 101 import com.android.calendar.event.AttendeesView; 102 import com.android.calendar.event.EditEventActivity; 103 import com.android.calendar.event.EditEventHelper; 104 import com.android.calendar.event.EventViewUtils; 105 import com.android.calendarcommon2.EventRecurrence; 106 107 import java.util.ArrayList; 108 import java.util.Arrays; 109 import java.util.Collections; 110 import java.util.List; 111 import java.util.regex.Pattern; 112 113 public 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 { QueryHandler(Context context)394 public QueryHandler(Context context) { 395 super(context); 396 } 397 398 @Override onQueryComplete(int token, Object cookie, Cursor cursor)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 sendAccessibilityEventIfQueryDone(int token)523 private void sendAccessibilityEventIfQueryDone(int token) { 524 mCurrentQuery |= token; 525 if (mCurrentQuery == TOKEN_QUERY_ALL) { 526 sendAccessibilityEvent(); 527 } 528 } 529 EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, int attendeeResponse, boolean isDialog, int windowStyle)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. EventInfoFragment()557 public EventInfoFragment() { 558 } 559 560 561 EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, int attendeeResponse, boolean isDialog, int windowStyle)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 onActivityCreated(Bundle savedInstanceState)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 applyDialogParams()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 setDialogParams(int x, int y, int minTop)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 onCheckedChanged(RadioGroup group, int checkedId)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 onNothingSelected(AdapterView<?> parent)657 public void onNothingSelected(AdapterView<?> parent) { 658 } 659 660 @Override onAttach(Activity activity)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 onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 updateTitle()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 */ initEventCursor()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") initAttendeesCursor(View view)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 onSaveInstanceState(Bundle outState)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 onCreateOptionsMenu(Menu menu, MenuInflater inflater)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 onOptionsItemSelected(MenuItem item)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 onDestroyView()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 onDestroy()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 */ saveResponse()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 updateResponse(long eventId, long attendeeId, int status)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 */ createExceptionResponse(long eventId, int status)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 getResponseFromButtonId(int buttonId)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 findButtonIdForResponse(int response)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 doEdit()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 updateEvent(View view)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 updateCustomAppButton()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 findNanpPhoneNumbers(CharSequence text)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 */ findNanpMatchEnd(CharSequence text, int startPos)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 indexFirstNonWhitespaceChar(CharSequence str)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 indexLastNonWhitespaceChar(CharSequence str)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 */ linkifyTextView(TextView textView)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 */ spanWillOverlap(Spannable spanText, URLSpan[] spanList, int start, int end)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 sendAccessibilityEvent()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 addFieldToAccessibilityEvent(List<CharSequence> text, TextView tv, ExpandableTextView etv)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 updateCalendar(View view)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 */ updateMenu()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 updateAttendees(View view)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 */ hasEmailableAttendees()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 hasEmailableOrganizer()1863 private boolean hasEmailableOrganizer() { 1864 return mEventOrganizerEmail != null && 1865 Utils.isEmailableFrom(mEventOrganizerEmail, mSyncAccountName); 1866 } 1867 initReminders(View view, Cursor cursor)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 updateResponse(View view)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 setTextCommon(View view, int id, CharSequence text)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 setVisibilityCommon(View view, int id, int visibility)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 */ showContactInfo(Attendee attendee, Rect rect)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 onPause()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 onResume()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 eventsChanged()2047 public void eventsChanged() { 2048 } 2049 2050 @Override getSupportedEventTypes()2051 public long getSupportedEventTypes() { 2052 return EventType.EVENTS_CHANGED; 2053 } 2054 2055 @Override handleEvent(EventInfo event)2056 public void handleEvent(EventInfo event) { 2057 if (event.eventType == EventType.EVENTS_CHANGED && mHandler != null) { 2058 // reload the data 2059 reloadEvents(); 2060 } 2061 } 2062 reloadEvents()2063 public void reloadEvents() { 2064 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 2065 null, null, null); 2066 } 2067 2068 @Override onClick(View view)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 */ addReminder()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 prepareReminders()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 saveReminders()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 */ emailAttendees()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 */ loadIntegerArray(Resources r, int resNum)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 */ loadStringArray(Resources r, int resNum)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 onDeleteStarted()2203 public void onDeleteStarted() { 2204 mEventDeletionStarted = true; 2205 } 2206 createDeleteOnDismissListener()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