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