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.FragmentManager; 31 import android.app.Service; 32 import android.content.ActivityNotFoundException; 33 import android.content.ContentProviderOperation; 34 import android.content.ContentResolver; 35 import android.content.ContentUris; 36 import android.content.ContentValues; 37 import android.content.Context; 38 import android.content.DialogInterface; 39 import android.content.Intent; 40 import android.content.SharedPreferences; 41 import android.content.pm.ApplicationInfo; 42 import android.content.pm.PackageManager; 43 import android.content.pm.PackageManager.NameNotFoundException; 44 import android.content.res.Resources; 45 import android.database.Cursor; 46 import android.graphics.Color; 47 import android.graphics.Rect; 48 import android.graphics.drawable.Drawable; 49 import android.net.Uri; 50 import android.os.Bundle; 51 import android.provider.CalendarContract; 52 import android.provider.CalendarContract.Attendees; 53 import android.provider.CalendarContract.Calendars; 54 import android.provider.CalendarContract.Events; 55 import android.provider.CalendarContract.Reminders; 56 import android.provider.ContactsContract; 57 import android.provider.ContactsContract.CommonDataKinds; 58 import android.provider.ContactsContract.Intents; 59 import android.provider.ContactsContract.QuickContact; 60 import android.text.Spannable; 61 import android.text.SpannableStringBuilder; 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.util.Rfc822Token; 68 import android.util.Log; 69 import android.util.SparseIntArray; 70 import android.view.Gravity; 71 import android.view.LayoutInflater; 72 import android.view.Menu; 73 import android.view.MenuInflater; 74 import android.view.MenuItem; 75 import android.view.MotionEvent; 76 import android.view.View; 77 import android.view.View.OnClickListener; 78 import android.view.View.OnTouchListener; 79 import android.view.ViewGroup; 80 import android.view.Window; 81 import android.view.WindowManager; 82 import android.view.accessibility.AccessibilityEvent; 83 import android.view.accessibility.AccessibilityManager; 84 import android.widget.AdapterView; 85 import android.widget.AdapterView.OnItemSelectedListener; 86 import android.widget.Button; 87 import android.widget.LinearLayout; 88 import android.widget.RadioButton; 89 import android.widget.RadioGroup; 90 import android.widget.RadioGroup.OnCheckedChangeListener; 91 import android.widget.ScrollView; 92 import android.widget.TextView; 93 import android.widget.Toast; 94 95 import com.android.calendar.CalendarController.EventInfo; 96 import com.android.calendar.CalendarController.EventType; 97 import com.android.calendar.alerts.QuickResponseActivity; 98 import com.android.calendarcommon2.DateException; 99 import com.android.calendarcommon2.Duration; 100 import com.android.calendarcommon2.EventRecurrence; 101 import com.android.colorpicker.HsvColorComparator; 102 103 import java.util.ArrayList; 104 import java.util.Arrays; 105 import java.util.Collections; 106 import java.util.List; 107 108 public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener, 109 CalendarController.EventHandler, OnClickListener { 110 111 public static final boolean DEBUG = false; 112 113 public static final String TAG = "EventInfoFragment"; 114 115 protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; 116 protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis"; 117 protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis"; 118 protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog"; 119 protected static final String BUNDLE_KEY_DELETE_DIALOG_VISIBLE = "key_delete_dialog_visible"; 120 protected static final String BUNDLE_KEY_WINDOW_STYLE = "key_window_style"; 121 protected static final String BUNDLE_KEY_CALENDAR_COLOR = "key_calendar_color"; 122 protected static final String BUNDLE_KEY_CALENDAR_COLOR_INIT = "key_calendar_color_init"; 123 protected static final String BUNDLE_KEY_CURRENT_COLOR = "key_current_color"; 124 protected static final String BUNDLE_KEY_CURRENT_COLOR_KEY = "key_current_color_key"; 125 protected static final String BUNDLE_KEY_CURRENT_COLOR_INIT = "key_current_color_init"; 126 protected static final String BUNDLE_KEY_ORIGINAL_COLOR = "key_original_color"; 127 protected static final String BUNDLE_KEY_ORIGINAL_COLOR_INIT = "key_original_color_init"; 128 protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response"; 129 protected static final String BUNDLE_KEY_USER_SET_ATTENDEE_RESPONSE = 130 "key_user_set_attendee_response"; 131 protected static final String BUNDLE_KEY_TENTATIVE_USER_RESPONSE = 132 "key_tentative_user_response"; 133 protected static final String BUNDLE_KEY_RESPONSE_WHICH_EVENTS = "key_response_which_events"; 134 protected static final String BUNDLE_KEY_REMINDER_MINUTES = "key_reminder_minutes"; 135 protected static final String BUNDLE_KEY_REMINDER_METHODS = "key_reminder_methods"; 136 137 138 private static final String PERIOD_SPACE = ". "; 139 140 private static final String NO_EVENT_COLOR = ""; 141 142 /** 143 * These are the corresponding indices into the array of strings 144 * "R.array.change_response_labels" in the resource file. 145 */ 146 static final int UPDATE_SINGLE = 0; 147 static final int UPDATE_ALL = 1; 148 149 // Style of view 150 public static final int FULL_WINDOW_STYLE = 0; 151 public static final int DIALOG_WINDOW_STYLE = 1; 152 153 private int mWindowStyle = DIALOG_WINDOW_STYLE; 154 155 // Query tokens for QueryHandler 156 private static final int TOKEN_QUERY_EVENT = 1 << 0; 157 private static final int TOKEN_QUERY_CALENDARS = 1 << 1; 158 private static final int TOKEN_QUERY_ATTENDEES = 1 << 2; 159 private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3; 160 private static final int TOKEN_QUERY_REMINDERS = 1 << 4; 161 private static final int TOKEN_QUERY_VISIBLE_CALENDARS = 1 << 5; 162 private static final int TOKEN_QUERY_COLORS = 1 << 6; 163 164 private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS 165 | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT 166 | TOKEN_QUERY_REMINDERS | TOKEN_QUERY_VISIBLE_CALENDARS | TOKEN_QUERY_COLORS; 167 168 private int mCurrentQuery = 0; 169 170 private static final String[] EVENT_PROJECTION = new String[] { 171 Events._ID, // 0 do not remove; used in DeleteEventHelper 172 Events.TITLE, // 1 do not remove; used in DeleteEventHelper 173 Events.RRULE, // 2 do not remove; used in DeleteEventHelper 174 Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper 175 Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper 176 Events.DTSTART, // 5 do not remove; used in DeleteEventHelper 177 Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper 178 Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper 179 Events.DESCRIPTION, // 8 180 Events.EVENT_LOCATION, // 9 181 Calendars.CALENDAR_ACCESS_LEVEL, // 10 182 Events.CALENDAR_COLOR, // 11 183 Events.EVENT_COLOR, // 12 184 Events.HAS_ATTENDEE_DATA, // 13 185 Events.ORGANIZER, // 14 186 Events.HAS_ALARM, // 15 187 Calendars.MAX_REMINDERS, // 16 188 Calendars.ALLOWED_REMINDERS, // 17 189 Events.CUSTOM_APP_PACKAGE, // 18 190 Events.CUSTOM_APP_URI, // 19 191 Events.DTEND, // 20 192 Events.DURATION, // 21 193 Events.ORIGINAL_SYNC_ID // 22 do not remove; used in DeleteEventHelper 194 }; 195 private static final int EVENT_INDEX_ID = 0; 196 private static final int EVENT_INDEX_TITLE = 1; 197 private static final int EVENT_INDEX_RRULE = 2; 198 private static final int EVENT_INDEX_ALL_DAY = 3; 199 private static final int EVENT_INDEX_CALENDAR_ID = 4; 200 private static final int EVENT_INDEX_DTSTART = 5; 201 private static final int EVENT_INDEX_SYNC_ID = 6; 202 private static final int EVENT_INDEX_EVENT_TIMEZONE = 7; 203 private static final int EVENT_INDEX_DESCRIPTION = 8; 204 private static final int EVENT_INDEX_EVENT_LOCATION = 9; 205 private static final int EVENT_INDEX_ACCESS_LEVEL = 10; 206 private static final int EVENT_INDEX_CALENDAR_COLOR = 11; 207 private static final int EVENT_INDEX_EVENT_COLOR = 12; 208 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 13; 209 private static final int EVENT_INDEX_ORGANIZER = 14; 210 private static final int EVENT_INDEX_HAS_ALARM = 15; 211 private static final int EVENT_INDEX_MAX_REMINDERS = 16; 212 private static final int EVENT_INDEX_ALLOWED_REMINDERS = 17; 213 private static final int EVENT_INDEX_CUSTOM_APP_PACKAGE = 18; 214 private static final int EVENT_INDEX_CUSTOM_APP_URI = 19; 215 private static final int EVENT_INDEX_DTEND = 20; 216 private static final int EVENT_INDEX_DURATION = 21; 217 218 static { 219 if (!Utils.isJellybeanOrLater()) { 220 EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_PACKAGE] = Events._ID; // dummy value 221 EVENT_PROJECTION[EVENT_INDEX_CUSTOM_APP_URI] = Events._ID; // dummy value 222 } 223 } 224 225 static final String[] CALENDARS_PROJECTION = new String[] { 226 Calendars._ID, // 0 227 Calendars.CALENDAR_DISPLAY_NAME, // 1 228 Calendars.OWNER_ACCOUNT, // 2 229 Calendars.CAN_ORGANIZER_RESPOND, // 3 230 Calendars.ACCOUNT_NAME, // 4 231 Calendars.ACCOUNT_TYPE // 5 232 }; 233 static final int CALENDARS_INDEX_DISPLAY_NAME = 1; 234 static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 235 static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3; 236 static final int CALENDARS_INDEX_ACCOUNT_NAME = 4; 237 static final int CALENDARS_INDEX_ACCOUNT_TYPE = 5; 238 239 static final String CALENDARS_WHERE = Calendars._ID + "=?"; 240 static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?"; 241 static final String CALENDARS_VISIBLE_WHERE = Calendars.VISIBLE + "=?"; 242 243 public static final int COLORS_INDEX_COLOR = 1; 244 public static final int COLORS_INDEX_COLOR_KEY = 2; 245 246 private View mView; 247 248 private Uri mUri; 249 private long mEventId; 250 private Cursor mEventCursor; 251 private Cursor mCalendarsCursor; 252 253 private static float mScale = 0; // Used for supporting different screen densities 254 255 private static int mCustomAppIconSize = 32; 256 257 private long mStartMillis; 258 private long mEndMillis; 259 private boolean mAllDay; 260 261 private boolean mOwnerCanRespond; 262 private String mSyncAccountName; 263 private String mCalendarOwnerAccount; 264 private boolean mIsBusyFreeCalendar; 265 266 private int mOriginalAttendeeResponse; 267 private int mAttendeeResponseFromIntent = Attendees.ATTENDEE_STATUS_NONE; 268 private int mUserSetResponse = Attendees.ATTENDEE_STATUS_NONE; 269 private int mWhichEvents = -1; 270 // Used as the temporary response until the dialog is confirmed. It is also 271 // able to be used as a state marker for configuration changes. 272 private int mTentativeUserSetResponse = Attendees.ATTENDEE_STATUS_NONE; 273 private boolean mHasAlarm; 274 // Used to prevent saving changes in event if it is being deleted. 275 private boolean mEventDeletionStarted = false; 276 277 private TextView mTitle; 278 private TextView mWhenDateTime; 279 private TextView mWhere; 280 private Menu mMenu = null; 281 private View mHeadlines; 282 private ScrollView mScrollView; 283 private View mLoadingMsgView; 284 private View mErrorMsgView; 285 private ObjectAnimator mAnimateAlpha; 286 private long mLoadingMsgStartTime; 287 288 private SparseIntArray mDisplayColorKeyMap = new SparseIntArray(); 289 private int mOriginalColor = -1; 290 private boolean mOriginalColorInitialized = false; 291 private int mCalendarColor = -1; 292 private boolean mCalendarColorInitialized = false; 293 private int mCurrentColor = -1; 294 private boolean mCurrentColorInitialized = false; 295 private int mCurrentColorKey = -1; 296 297 private static final int FADE_IN_TIME = 300; // in milliseconds 298 private static final int LOADING_MSG_DELAY = 600; // in milliseconds 299 private static final int LOADING_MSG_MIN_DISPLAY_TIME = 600; 300 private boolean mNoCrossFade = false; // Used to prevent repeated cross-fade 301 private RadioGroup mResponseRadioGroup; 302 303 ArrayList<String> mToEmails = new ArrayList<String>(); 304 ArrayList<String> mCcEmails = new ArrayList<String>(); 305 306 307 private final Runnable mTZUpdater = new Runnable() { 308 @Override 309 public void run() { 310 updateEvent(mView); 311 } 312 }; 313 314 private final Runnable mLoadingMsgAlphaUpdater = new Runnable() { 315 @Override 316 public void run() { 317 // Since this is run after a delay, make sure to only show the message 318 // if the event's data is not shown yet. 319 if (!mAnimateAlpha.isRunning() && mScrollView.getAlpha() == 0) { 320 mLoadingMsgStartTime = System.currentTimeMillis(); 321 mLoadingMsgView.setAlpha(1); 322 } 323 } 324 }; 325 326 private static int mDialogWidth = 500; 327 private static int mDialogHeight = 600; 328 private static int DIALOG_TOP_MARGIN = 8; 329 private boolean mIsDialog = false; 330 private boolean mIsPaused = true; 331 private boolean mDismissOnResume = false; 332 private int mX = -1; 333 private int mY = -1; 334 private int mMinTop; // Dialog cannot be above this location 335 private boolean mIsTabletConfig; 336 private Activity mActivity; 337 private Context mContext; 338 339 private CalendarController mController; 340 sendAccessibilityEventIfQueryDone(int token)341 private void sendAccessibilityEventIfQueryDone(int token) { 342 mCurrentQuery |= token; 343 if (mCurrentQuery == TOKEN_QUERY_ALL) { 344 sendAccessibilityEvent(); 345 } 346 } 347 EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, int attendeeResponse, boolean isDialog, int windowStyle)348 public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, 349 int attendeeResponse, boolean isDialog, int windowStyle) { 350 351 Resources r = context.getResources(); 352 if (mScale == 0) { 353 mScale = context.getResources().getDisplayMetrics().density; 354 if (mScale != 1) { 355 mCustomAppIconSize *= mScale; 356 if (isDialog) { 357 DIALOG_TOP_MARGIN *= mScale; 358 } 359 } 360 } 361 if (isDialog) { 362 setDialogSize(r); 363 } 364 mIsDialog = isDialog; 365 366 setStyle(DialogFragment.STYLE_NO_TITLE, 0); 367 mUri = uri; 368 mStartMillis = startMillis; 369 mEndMillis = endMillis; 370 mAttendeeResponseFromIntent = attendeeResponse; 371 mWindowStyle = windowStyle; 372 } 373 374 // This is currently required by the fragment manager. EventInfoFragment()375 public EventInfoFragment() { 376 } 377 EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, int attendeeResponse, boolean isDialog, int windowStyle)378 public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, 379 int attendeeResponse, boolean isDialog, int windowStyle) { 380 this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis, 381 endMillis, attendeeResponse, isDialog, windowStyle); 382 mEventId = eventId; 383 } 384 385 @Override onActivityCreated(Bundle savedInstanceState)386 public void onActivityCreated(Bundle savedInstanceState) { 387 super.onActivityCreated(savedInstanceState); 388 389 if (mIsDialog) { 390 applyDialogParams(); 391 } 392 393 final Activity activity = getActivity(); 394 mContext = activity; 395 } 396 applyDialogParams()397 private void applyDialogParams() { 398 Dialog dialog = getDialog(); 399 dialog.setCanceledOnTouchOutside(true); 400 401 Window window = dialog.getWindow(); 402 window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 403 404 WindowManager.LayoutParams a = window.getAttributes(); 405 a.dimAmount = .4f; 406 407 a.width = mDialogWidth; 408 a.height = mDialogHeight; 409 410 411 // On tablets , do smart positioning of dialog 412 // On phones , use the whole screen 413 414 if (mX != -1 || mY != -1) { 415 a.x = mX - mDialogWidth / 2; 416 a.y = mY - mDialogHeight / 2; 417 if (a.y < mMinTop) { 418 a.y = mMinTop + DIALOG_TOP_MARGIN; 419 } 420 a.gravity = Gravity.LEFT | Gravity.TOP; 421 } 422 window.setAttributes(a); 423 } 424 setDialogParams(int x, int y, int minTop)425 public void setDialogParams(int x, int y, int minTop) { 426 mX = x; 427 mY = y; 428 mMinTop = minTop; 429 } 430 431 // Implements OnCheckedChangeListener 432 @Override onCheckedChanged(RadioGroup group, int checkedId)433 public void onCheckedChanged(RadioGroup group, int checkedId) { 434 } 435 onNothingSelected(AdapterView<?> parent)436 public void onNothingSelected(AdapterView<?> parent) { 437 } 438 439 @Override onDetach()440 public void onDetach() { 441 super.onDetach(); 442 mController.deregisterEventHandler(R.layout.event_info); 443 } 444 445 @Override onAttach(Activity activity)446 public void onAttach(Activity activity) { 447 super.onAttach(activity); 448 mActivity = activity; 449 // Ensure that mIsTabletConfig is set before creating the menu. 450 mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config); 451 mController = CalendarController.getInstance(mActivity); 452 mController.registerEventHandler(R.layout.event_info, this); 453 454 if (!mIsDialog) { 455 setHasOptionsMenu(true); 456 } 457 } 458 459 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)460 public View onCreateView(LayoutInflater inflater, ViewGroup container, 461 Bundle savedInstanceState) { 462 if (mWindowStyle == DIALOG_WINDOW_STYLE) { 463 mView = inflater.inflate(R.layout.event_info_dialog, container, false); 464 } else { 465 mView = inflater.inflate(R.layout.event_info, container, false); 466 } 467 mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view); 468 mLoadingMsgView = mView.findViewById(R.id.event_info_loading_msg); 469 mErrorMsgView = mView.findViewById(R.id.event_info_error_msg); 470 mTitle = (TextView) mView.findViewById(R.id.title); 471 mWhenDateTime = (TextView) mView.findViewById(R.id.when_datetime); 472 mWhere = (TextView) mView.findViewById(R.id.where); 473 mHeadlines = mView.findViewById(R.id.event_info_headline); 474 475 mResponseRadioGroup = (RadioGroup) mView.findViewById(R.id.response_value); 476 477 mAnimateAlpha = ObjectAnimator.ofFloat(mScrollView, "Alpha", 0, 1); 478 mAnimateAlpha.setDuration(FADE_IN_TIME); 479 mAnimateAlpha.addListener(new AnimatorListenerAdapter() { 480 int defLayerType; 481 482 @Override 483 public void onAnimationStart(Animator animation) { 484 // Use hardware layer for better performance during animation 485 defLayerType = mScrollView.getLayerType(); 486 mScrollView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 487 // Ensure that the loading message is gone before showing the 488 // event info 489 mLoadingMsgView.removeCallbacks(mLoadingMsgAlphaUpdater); 490 mLoadingMsgView.setVisibility(View.GONE); 491 } 492 493 @Override 494 public void onAnimationCancel(Animator animation) { 495 mScrollView.setLayerType(defLayerType, null); 496 } 497 498 @Override 499 public void onAnimationEnd(Animator animation) { 500 mScrollView.setLayerType(defLayerType, null); 501 // Do not cross fade after the first time 502 mNoCrossFade = true; 503 } 504 }); 505 506 mLoadingMsgView.setAlpha(0); 507 mScrollView.setAlpha(0); 508 mErrorMsgView.setVisibility(View.INVISIBLE); 509 mLoadingMsgView.postDelayed(mLoadingMsgAlphaUpdater, LOADING_MSG_DELAY); 510 511 // Hide Edit/Delete buttons if in full screen mode on a phone 512 if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) { 513 mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE); 514 } 515 516 return mView; 517 } 518 updateTitle()519 private void updateTitle() { 520 Resources res = getActivity().getResources(); 521 getActivity().setTitle(res.getString(R.string.event_info_title)); 522 } 523 524 /** 525 * Initializes the event cursor, which is expected to point to the first 526 * (and only) result from a query. 527 * @return false if the cursor is empty, true otherwise 528 */ initEventCursor()529 private boolean initEventCursor() { 530 if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) { 531 return false; 532 } 533 mEventCursor.moveToFirst(); 534 mEventId = mEventCursor.getInt(EVENT_INDEX_ID); 535 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 536 // mHasAlarm will be true if it was saved in the event already. 537 mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)? true : false; 538 return true; 539 } 540 541 @Override onSaveInstanceState(Bundle outState)542 public void onSaveInstanceState(Bundle outState) { 543 super.onSaveInstanceState(outState); 544 } 545 546 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)547 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 548 super.onCreateOptionsMenu(menu, inflater); 549 // Show color/edit/delete buttons only in non-dialog configuration 550 if (!mIsDialog && !mIsTabletConfig || mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) { 551 inflater.inflate(R.menu.event_info_title_bar, menu); 552 mMenu = menu; 553 } 554 } 555 556 @Override onOptionsItemSelected(MenuItem item)557 public boolean onOptionsItemSelected(MenuItem item) { 558 559 // If we're a dialog we don't want to handle menu buttons 560 if (mIsDialog) { 561 return false; 562 } 563 // Handles option menu selections: 564 // Home button - close event info activity and start the main calendar 565 // one 566 // Edit button - start the event edit activity and close the info 567 // activity 568 // Delete button - start a delete query that calls a runnable that close 569 // the info activity 570 571 final int itemId = item.getItemId(); 572 if (itemId == android.R.id.home) { 573 Utils.returnToCalendarHome(mContext); 574 mActivity.finish(); 575 return true; 576 } else if (itemId == R.id.info_action_edit) { 577 mActivity.finish(); 578 } 579 return super.onOptionsItemSelected(item); 580 } 581 582 @Override onStop()583 public void onStop() { 584 super.onStop(); 585 } 586 587 @Override onDestroy()588 public void onDestroy() { 589 if (mEventCursor != null) { 590 mEventCursor.close(); 591 } 592 if (mCalendarsCursor != null) { 593 mCalendarsCursor.close(); 594 } 595 super.onDestroy(); 596 } 597 598 /** 599 * Creates an exception to a recurring event. The only change we're making is to the 600 * "self attendee status" value. The provider will take care of updating the corresponding 601 * Attendees.attendeeStatus entry. 602 * 603 * @param eventId The recurring event. 604 * @param status The new value for selfAttendeeStatus. 605 */ createExceptionResponse(long eventId, int status)606 private void createExceptionResponse(long eventId, int status) { 607 ContentValues values = new ContentValues(); 608 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis); 609 values.put(Events.SELF_ATTENDEE_STATUS, status); 610 values.put(Events.STATUS, Events.STATUS_CONFIRMED); 611 612 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 613 Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, 614 String.valueOf(eventId)); 615 ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build()); 616 } 617 getResponseFromButtonId(int buttonId)618 public static int getResponseFromButtonId(int buttonId) { 619 return Attendees.ATTENDEE_STATUS_NONE; 620 } 621 findButtonIdForResponse(int response)622 public static int findButtonIdForResponse(int response) { 623 return -1; 624 } 625 displayEventNotFound()626 private void displayEventNotFound() { 627 mErrorMsgView.setVisibility(View.VISIBLE); 628 mScrollView.setVisibility(View.GONE); 629 mLoadingMsgView.setVisibility(View.GONE); 630 } 631 updateEvent(View view)632 private void updateEvent(View view) { 633 if (mEventCursor == null || view == null) { 634 return; 635 } 636 637 Context context = view.getContext(); 638 if (context == null) { 639 return; 640 } 641 642 String eventName = mEventCursor.getString(EVENT_INDEX_TITLE); 643 if (eventName == null || eventName.length() == 0) { 644 eventName = getActivity().getString(R.string.no_title_label); 645 } 646 647 // 3rd parties might not have specified the start/end time when firing the 648 // Events.CONTENT_URI intent. Update these with values read from the db. 649 if (mStartMillis == 0 && mEndMillis == 0) { 650 mStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART); 651 mEndMillis = mEventCursor.getLong(EVENT_INDEX_DTEND); 652 if (mEndMillis == 0) { 653 String duration = mEventCursor.getString(EVENT_INDEX_DURATION); 654 if (!TextUtils.isEmpty(duration)) { 655 try { 656 Duration d = new Duration(); 657 d.parse(duration); 658 long endMillis = mStartMillis + d.getMillis(); 659 if (endMillis >= mStartMillis) { 660 mEndMillis = endMillis; 661 } else { 662 Log.d(TAG, "Invalid duration string: " + duration); 663 } 664 } catch (DateException e) { 665 Log.d(TAG, "Error parsing duration string " + duration, e); 666 } 667 } 668 if (mEndMillis == 0) { 669 mEndMillis = mStartMillis; 670 } 671 } 672 } 673 674 mAllDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 675 String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION); 676 String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION); 677 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 678 String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); 679 680 mHeadlines.setBackgroundColor(mCurrentColor); 681 682 // What 683 if (eventName != null) { 684 setTextCommon(view, R.id.title, eventName); 685 } 686 687 // When 688 // Set the date and repeats (if any) 689 String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater); 690 691 Resources resources = context.getResources(); 692 String displayedDatetime = Utils.getDisplayedDatetime(mStartMillis, mEndMillis, 693 System.currentTimeMillis(), localTimezone, mAllDay, context); 694 695 String displayedTimezone = null; 696 if (!mAllDay) { 697 displayedTimezone = Utils.getDisplayedTimezone(mStartMillis, localTimezone, 698 eventTimezone); 699 } 700 // Display the datetime. Make the timezone (if any) transparent. 701 if (displayedTimezone == null) { 702 setTextCommon(view, R.id.when_datetime, displayedDatetime); 703 } else { 704 int timezoneIndex = displayedDatetime.length(); 705 displayedDatetime += " " + displayedTimezone; 706 SpannableStringBuilder sb = new SpannableStringBuilder(displayedDatetime); 707 ForegroundColorSpan transparentColorSpan = new ForegroundColorSpan( 708 resources.getColor(R.color.event_info_headline_transparent_color)); 709 sb.setSpan(transparentColorSpan, timezoneIndex, displayedDatetime.length(), 710 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 711 setTextCommon(view, R.id.when_datetime, sb); 712 } 713 714 view.findViewById(R.id.when_repeat).setVisibility(View.GONE); 715 716 // Organizer view is setup in the updateCalendar method 717 718 719 // Where 720 if (location == null || location.trim().length() == 0) { 721 setVisibilityCommon(view, R.id.where, View.GONE); 722 } else { 723 final TextView textView = mWhere; 724 if (textView != null) { 725 textView.setText(location.trim()); 726 } 727 } 728 729 // Launch Custom App 730 if (Utils.isJellybeanOrLater()) { 731 updateCustomAppButton(); 732 } 733 } 734 updateCustomAppButton()735 private void updateCustomAppButton() { 736 setVisibilityCommon(mView, R.id.launch_custom_app_container, View.GONE); 737 return; 738 } 739 sendAccessibilityEvent()740 private void sendAccessibilityEvent() { 741 AccessibilityManager am = 742 (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE); 743 if (!am.isEnabled()) { 744 return; 745 } 746 747 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 748 event.setClassName(EventInfoFragment.class.getName()); 749 event.setPackageName(getActivity().getPackageName()); 750 List<CharSequence> text = event.getText(); 751 752 if (mResponseRadioGroup.getVisibility() == View.VISIBLE) { 753 int id = mResponseRadioGroup.getCheckedRadioButtonId(); 754 if (id != View.NO_ID) { 755 text.add(((TextView) getView().findViewById(R.id.response_label)).getText()); 756 text.add((((RadioButton) (mResponseRadioGroup.findViewById(id))) 757 .getText() + PERIOD_SPACE)); 758 } 759 } 760 761 am.sendAccessibilityEvent(event); 762 } 763 updateCalendar(View view)764 private void updateCalendar(View view) { 765 766 mCalendarOwnerAccount = ""; 767 if (mCalendarsCursor != null && mEventCursor != null) { 768 mCalendarsCursor.moveToFirst(); 769 String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 770 mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount; 771 mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0; 772 mSyncAccountName = mCalendarsCursor.getString(CALENDARS_INDEX_ACCOUNT_NAME); 773 774 setVisibilityCommon(view, R.id.organizer_container, View.GONE); 775 mIsBusyFreeCalendar = 776 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY; 777 778 if (!mIsBusyFreeCalendar) { 779 780 View b = mView.findViewById(R.id.edit); 781 b.setEnabled(true); 782 b.setOnClickListener(new OnClickListener() { 783 @Override 784 public void onClick(View v) { 785 // For dialogs, just close the fragment 786 // For full screen, close activity on phone, leave it for tablet 787 if (mIsDialog) { 788 EventInfoFragment.this.dismiss(); 789 } 790 else if (!mIsTabletConfig){ 791 getActivity().finish(); 792 } 793 } 794 }); 795 } 796 View button; 797 if ((!mIsDialog && !mIsTabletConfig || 798 mWindowStyle == EventInfoFragment.FULL_WINDOW_STYLE) && mMenu != null) { 799 mActivity.invalidateOptionsMenu(); 800 } 801 } else { 802 setVisibilityCommon(view, R.id.calendar, View.GONE); 803 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); 804 } 805 } 806 setTextCommon(View view, int id, CharSequence text)807 private void setTextCommon(View view, int id, CharSequence text) { 808 TextView textView = (TextView) view.findViewById(id); 809 if (textView == null) 810 return; 811 textView.setText(text); 812 } 813 setVisibilityCommon(View view, int id, int visibility)814 private void setVisibilityCommon(View view, int id, int visibility) { 815 View v = view.findViewById(id); 816 if (v != null) { 817 v.setVisibility(visibility); 818 } 819 return; 820 } 821 822 @Override onPause()823 public void onPause() { 824 mIsPaused = true; 825 super.onPause(); 826 } 827 828 @Override onResume()829 public void onResume() { 830 super.onResume(); 831 if (mIsDialog) { 832 setDialogSize(getActivity().getResources()); 833 applyDialogParams(); 834 } 835 mIsPaused = false; 836 if (mTentativeUserSetResponse != Attendees.ATTENDEE_STATUS_NONE) { 837 int buttonId = findButtonIdForResponse(mTentativeUserSetResponse); 838 mResponseRadioGroup.check(buttonId); 839 } 840 } 841 842 @Override eventsChanged()843 public void eventsChanged() { 844 } 845 846 @Override getSupportedEventTypes()847 public long getSupportedEventTypes() { 848 return EventType.EVENTS_CHANGED; 849 } 850 851 @Override handleEvent(EventInfo event)852 public void handleEvent(EventInfo event) { 853 reloadEvents(); 854 } 855 reloadEvents()856 public void reloadEvents() { 857 } 858 859 @Override onClick(View view)860 public void onClick(View view) { 861 } 862 getEventId()863 public long getEventId() { 864 return mEventId; 865 } 866 getStartMillis()867 public long getStartMillis() { 868 return mStartMillis; 869 } getEndMillis()870 public long getEndMillis() { 871 return mEndMillis; 872 } setDialogSize(Resources r)873 private void setDialogSize(Resources r) { 874 mDialogWidth = (int)r.getDimension(R.dimen.event_info_dialog_width); 875 mDialogHeight = (int)r.getDimension(R.dimen.event_info_dialog_height); 876 } 877 } 878