EventInfoFragment.java revision d8353fb85ef59a88580125207be9c914a8586758
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.calendar; 18 19import static android.provider.CalendarContract.EXTRA_EVENT_BEGIN_TIME; 20import static android.provider.CalendarContract.EXTRA_EVENT_END_TIME; 21import static com.android.calendar.CalendarController.EVENT_EDIT_ON_LAUNCH; 22 23import com.android.calendar.CalendarController.EventInfo; 24import com.android.calendar.CalendarController.EventType; 25import com.android.calendar.CalendarEventModel.Attendee; 26import com.android.calendar.CalendarEventModel.ReminderEntry; 27import com.android.calendar.event.AttendeesView; 28import com.android.calendar.event.EditEventActivity; 29import com.android.calendar.event.EditEventHelper; 30import com.android.calendarcommon.EventRecurrence; 31import com.android.calendar.event.EventViewUtils; 32 33import android.app.Activity; 34import android.app.Dialog; 35import android.app.DialogFragment; 36import android.app.Service; 37import android.content.ActivityNotFoundException; 38import android.content.ContentProviderOperation; 39import android.content.ContentResolver; 40import android.content.ContentUris; 41import android.content.ContentValues; 42import android.content.Context; 43import android.content.Intent; 44import android.content.SharedPreferences; 45import android.content.res.Resources; 46import android.database.Cursor; 47import android.graphics.Rect; 48import android.graphics.Typeface; 49import android.net.Uri; 50import android.os.Bundle; 51import android.provider.CalendarContract; 52import android.provider.CalendarContract.Attendees; 53import android.provider.CalendarContract.Calendars; 54import android.provider.CalendarContract.Events; 55import android.provider.CalendarContract.Reminders; 56import android.provider.ContactsContract; 57import android.provider.ContactsContract.CommonDataKinds; 58import android.provider.ContactsContract.Intents; 59import android.provider.ContactsContract.QuickContact; 60import android.text.Spannable; 61import android.text.SpannableStringBuilder; 62import android.text.TextUtils; 63import android.text.format.DateFormat; 64import android.text.format.DateUtils; 65import android.text.format.Time; 66import android.text.style.ForegroundColorSpan; 67import android.text.style.StrikethroughSpan; 68import android.text.style.StyleSpan; 69import android.text.util.Linkify; 70import android.text.util.Rfc822Token; 71import android.util.Log; 72import android.view.Gravity; 73import android.view.LayoutInflater; 74import android.view.Menu; 75import android.view.MenuInflater; 76import android.view.MenuItem; 77import android.view.MotionEvent; 78import android.view.View; 79import android.view.View.OnClickListener; 80import android.view.View.OnTouchListener; 81import android.view.ViewGroup; 82import android.view.Window; 83import android.view.WindowManager; 84import android.view.accessibility.AccessibilityEvent; 85import android.view.accessibility.AccessibilityManager; 86import android.widget.AdapterView; 87import android.widget.Button; 88import android.widget.ImageButton; 89import android.widget.LinearLayout; 90import android.widget.RadioButton; 91import android.widget.RadioGroup; 92import android.widget.ScrollView; 93import android.widget.RadioGroup.OnCheckedChangeListener; 94import android.widget.TextView; 95import android.widget.Toast; 96 97import java.util.ArrayList; 98import java.util.Arrays; 99import java.util.Collections; 100import java.util.Formatter; 101import java.util.List; 102import java.util.Locale; 103import java.util.regex.Pattern; 104import java.util.TimeZone; 105 106 107public class EventInfoFragment extends DialogFragment implements OnCheckedChangeListener, 108 CalendarController.EventHandler, OnClickListener { 109 public static final boolean DEBUG = false; 110 111 public static final String TAG = "EventInfoFragment"; 112 113 protected static final String BUNDLE_KEY_EVENT_ID = "key_event_id"; 114 protected static final String BUNDLE_KEY_START_MILLIS = "key_start_millis"; 115 protected static final String BUNDLE_KEY_END_MILLIS = "key_end_millis"; 116 protected static final String BUNDLE_KEY_IS_DIALOG = "key_fragment_is_dialog"; 117 protected static final String BUNDLE_KEY_ATTENDEE_RESPONSE = "key_attendee_response"; 118 119 private static final String PERIOD_SPACE = ". "; 120 121 /** 122 * These are the corresponding indices into the array of strings 123 * "R.array.change_response_labels" in the resource file. 124 */ 125 static final int UPDATE_SINGLE = 0; 126 static final int UPDATE_ALL = 1; 127 128 // Query tokens for QueryHandler 129 private static final int TOKEN_QUERY_EVENT = 1 << 0; 130 private static final int TOKEN_QUERY_CALENDARS = 1 << 1; 131 private static final int TOKEN_QUERY_ATTENDEES = 1 << 2; 132 private static final int TOKEN_QUERY_DUPLICATE_CALENDARS = 1 << 3; 133 private static final int TOKEN_QUERY_REMINDERS = 1 << 4; 134 private static final int TOKEN_QUERY_ALL = TOKEN_QUERY_DUPLICATE_CALENDARS 135 | TOKEN_QUERY_ATTENDEES | TOKEN_QUERY_CALENDARS | TOKEN_QUERY_EVENT 136 | TOKEN_QUERY_REMINDERS; 137 private int mCurrentQuery = 0; 138 139 private static final String[] EVENT_PROJECTION = new String[] { 140 Events._ID, // 0 do not remove; used in DeleteEventHelper 141 Events.TITLE, // 1 do not remove; used in DeleteEventHelper 142 Events.RRULE, // 2 do not remove; used in DeleteEventHelper 143 Events.ALL_DAY, // 3 do not remove; used in DeleteEventHelper 144 Events.CALENDAR_ID, // 4 do not remove; used in DeleteEventHelper 145 Events.DTSTART, // 5 do not remove; used in DeleteEventHelper 146 Events._SYNC_ID, // 6 do not remove; used in DeleteEventHelper 147 Events.EVENT_TIMEZONE, // 7 do not remove; used in DeleteEventHelper 148 Events.DESCRIPTION, // 8 149 Events.EVENT_LOCATION, // 9 150 Calendars.CALENDAR_ACCESS_LEVEL, // 10 151 Calendars.CALENDAR_COLOR, // 11 152 Events.HAS_ATTENDEE_DATA, // 12 153 Events.ORGANIZER, // 13 154 Events.HAS_ALARM, // 14 155 Calendars.MAX_REMINDERS, //15 156 Calendars.ALLOWED_REMINDERS, // 16 157 Events.ORIGINAL_SYNC_ID // 17 do not remove; used in DeleteEventHelper 158 }; 159 private static final int EVENT_INDEX_ID = 0; 160 private static final int EVENT_INDEX_TITLE = 1; 161 private static final int EVENT_INDEX_RRULE = 2; 162 private static final int EVENT_INDEX_ALL_DAY = 3; 163 private static final int EVENT_INDEX_CALENDAR_ID = 4; 164 private static final int EVENT_INDEX_SYNC_ID = 6; 165 private static final int EVENT_INDEX_EVENT_TIMEZONE = 7; 166 private static final int EVENT_INDEX_DESCRIPTION = 8; 167 private static final int EVENT_INDEX_EVENT_LOCATION = 9; 168 private static final int EVENT_INDEX_ACCESS_LEVEL = 10; 169 private static final int EVENT_INDEX_COLOR = 11; 170 private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 12; 171 private static final int EVENT_INDEX_ORGANIZER = 13; 172 private static final int EVENT_INDEX_HAS_ALARM = 14; 173 private static final int EVENT_INDEX_MAX_REMINDERS = 15; 174 private static final int EVENT_INDEX_ALLOWED_REMINDERS = 16; 175 176 177 private static final String[] ATTENDEES_PROJECTION = new String[] { 178 Attendees._ID, // 0 179 Attendees.ATTENDEE_NAME, // 1 180 Attendees.ATTENDEE_EMAIL, // 2 181 Attendees.ATTENDEE_RELATIONSHIP, // 3 182 Attendees.ATTENDEE_STATUS, // 4 183 }; 184 private static final int ATTENDEES_INDEX_ID = 0; 185 private static final int ATTENDEES_INDEX_NAME = 1; 186 private static final int ATTENDEES_INDEX_EMAIL = 2; 187 private static final int ATTENDEES_INDEX_RELATIONSHIP = 3; 188 private static final int ATTENDEES_INDEX_STATUS = 4; 189 190 private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; 191 192 private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, " 193 + Attendees.ATTENDEE_EMAIL + " ASC"; 194 195 private static final String[] REMINDERS_PROJECTION = new String[] { 196 Reminders._ID, // 0 197 Reminders.MINUTES, // 1 198 Reminders.METHOD // 2 199 }; 200 private static final int REMINDERS_INDEX_ID = 0; 201 private static final int REMINDERS_MINUTES_ID = 1; 202 private static final int REMINDERS_METHOD_ID = 2; 203 204 private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=?"; 205 206 static final String[] CALENDARS_PROJECTION = new String[] { 207 Calendars._ID, // 0 208 Calendars.CALENDAR_DISPLAY_NAME, // 1 209 Calendars.OWNER_ACCOUNT, // 2 210 Calendars.CAN_ORGANIZER_RESPOND // 3 211 }; 212 static final int CALENDARS_INDEX_DISPLAY_NAME = 1; 213 static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2; 214 static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3; 215 216 static final String CALENDARS_WHERE = Calendars._ID + "=?"; 217 static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.CALENDAR_DISPLAY_NAME + "=?"; 218 219 private View mView; 220 221 private Uri mUri; 222 private long mEventId; 223 private Cursor mEventCursor; 224 private Cursor mAttendeesCursor; 225 private Cursor mCalendarsCursor; 226 private Cursor mRemindersCursor; 227 228 private static float mScale = 0; // Used for supporting different screen densities 229 230 private long mStartMillis; 231 private long mEndMillis; 232 233 private boolean mHasAttendeeData; 234 private boolean mIsOrganizer; 235 private long mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 236 private boolean mOwnerCanRespond; 237 private String mCalendarOwnerAccount; 238 private boolean mCanModifyCalendar; 239 private boolean mIsBusyFreeCalendar; 240 private int mNumOfAttendees; 241 242 private EditResponseHelper mEditResponseHelper; 243 244 private int mOriginalAttendeeResponse; 245 private int mAttendeeResponseFromIntent = CalendarController.ATTENDEE_NO_RESPONSE; 246 private boolean mIsRepeating; 247 private boolean mHasAlarm; 248 private int mMaxReminders; 249 private String mCalendarAllowedReminders; 250 251 private TextView mTitle; 252 private TextView mWhen; 253 private TextView mWhere; 254 private TextView mWhat; 255 private TextView mAttendees; 256 private AttendeesView mLongAttendees; 257 private Menu mMenu = null; 258 private View mHeadlines; 259 private ScrollView mScrollView; 260 261 private Pattern mWildcardPattern = Pattern.compile("^.*$"); 262 263 ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>(); 264 ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>(); 265 ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>(); 266 ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>(); 267 private int mColor; 268 269 270 private int mDefaultReminderMinutes; 271 private ArrayList<LinearLayout> mReminderViews = new ArrayList<LinearLayout>(0); 272 public ArrayList<ReminderEntry> mReminders; 273 public ArrayList<ReminderEntry> mOriginalReminders; 274 275 /** 276 * Contents of the "minutes" spinner. This has default values from the XML file, augmented 277 * with any additional values that were already associated with the event. 278 */ 279 private ArrayList<Integer> mReminderMinuteValues; 280 private ArrayList<String> mReminderMinuteLabels; 281 282 /** 283 * Contents of the "methods" spinner. The "values" list specifies the method constant 284 * (e.g. {@link Reminders#METHOD_ALERT}) associated with the labels. Any methods that 285 * aren't allowed by the Calendar will be removed. 286 */ 287 private ArrayList<Integer> mReminderMethodValues; 288 private ArrayList<String> mReminderMethodLabels; 289 290 291 292 private QueryHandler mHandler; 293 294 private Runnable mTZUpdater = new Runnable() { 295 @Override 296 public void run() { 297 updateEvent(mView); 298 } 299 }; 300 301 private static int DIALOG_WIDTH = 500; 302 private static int DIALOG_HEIGHT = 600; 303 private boolean mIsDialog = false; 304 private boolean mIsPaused = true; 305 private boolean mDismissOnResume = false; 306 private int mX = -1; 307 private int mY = -1; 308 private Button mDescButton; // Button to expand/collapse the description 309 private String mMoreLabel; // Labels for the button 310 private String mLessLabel; 311 private boolean mShowMaxDescription; // Current status of button 312 private int mDescLineNum; // The default number of lines in the description 313 private boolean mIsTabletConfig; 314 private Activity mActivity; 315 316 private class QueryHandler extends AsyncQueryService { 317 public QueryHandler(Context context) { 318 super(context); 319 } 320 321 @Override 322 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 323 // if the activity is finishing, then close the cursor and return 324 final Activity activity = getActivity(); 325 if (activity == null || activity.isFinishing()) { 326 cursor.close(); 327 return; 328 } 329 330 switch (token) { 331 case TOKEN_QUERY_EVENT: 332 mEventCursor = Utils.matrixCursorFromCursor(cursor); 333 if (initEventCursor()) { 334 // The cursor is empty. This can happen if the event was 335 // deleted. 336 // FRAG_TODO we should no longer rely on Activity.finish() 337 activity.finish(); 338 return; 339 } 340 updateEvent(mView); 341 342 // start calendar query 343 Uri uri = Calendars.CONTENT_URI; 344 String[] args = new String[] { 345 Long.toString(mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID))}; 346 startQuery(TOKEN_QUERY_CALENDARS, null, uri, CALENDARS_PROJECTION, 347 CALENDARS_WHERE, args, null); 348 break; 349 case TOKEN_QUERY_CALENDARS: 350 mCalendarsCursor = Utils.matrixCursorFromCursor(cursor); 351 updateCalendar(mView); 352 // FRAG_TODO fragments shouldn't set the title anymore 353 updateTitle(); 354 355 if (!mIsBusyFreeCalendar) { 356 args = new String[] { Long.toString(mEventId) }; 357 358 // start attendees query 359 uri = Attendees.CONTENT_URI; 360 startQuery(TOKEN_QUERY_ATTENDEES, null, uri, ATTENDEES_PROJECTION, 361 ATTENDEES_WHERE, args, ATTENDEES_SORT_ORDER); 362 } else { 363 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_ATTENDEES); 364 } 365 mOriginalReminders = new ArrayList<ReminderEntry> (); 366 if (mHasAlarm) { 367 // start reminders query 368 args = new String[] { Long.toString(mEventId) }; 369 uri = Reminders.CONTENT_URI; 370 startQuery(TOKEN_QUERY_REMINDERS, null, uri, 371 REMINDERS_PROJECTION, REMINDERS_WHERE, args, null); 372 } else { 373 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_REMINDERS); 374 } 375 break; 376 case TOKEN_QUERY_ATTENDEES: 377 mAttendeesCursor = Utils.matrixCursorFromCursor(cursor); 378 initAttendeesCursor(mView); 379 updateResponse(mView); 380 break; 381 case TOKEN_QUERY_REMINDERS: 382 mRemindersCursor = Utils.matrixCursorFromCursor(cursor); 383 initReminders(mView, mRemindersCursor); 384 break; 385 case TOKEN_QUERY_DUPLICATE_CALENDARS: 386 Resources res = activity.getResources(); 387 SpannableStringBuilder sb = new SpannableStringBuilder(); 388 389 // Label 390 String label = res.getString(R.string.view_event_calendar_label); 391 sb.append(label).append(" "); 392 sb.setSpan(new StyleSpan(Typeface.BOLD), 0, label.length(), 393 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 394 395 // Calendar display name 396 String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 397 sb.append(calendarName); 398 399 // Show email account if display name is not unique and 400 // display name != email 401 String email = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 402 if (cursor.getCount() > 1 && !calendarName.equalsIgnoreCase(email)) { 403 sb.append(" (").append(email).append(")"); 404 } 405 406 break; 407 } 408 cursor.close(); 409 sendAccessibilityEventIfQueryDone(token); 410 } 411 412 } 413 414 private void sendAccessibilityEventIfQueryDone(int token) { 415 mCurrentQuery |= token; 416 if (mCurrentQuery == TOKEN_QUERY_ALL) { 417 sendAccessibilityEvent(); 418 } 419 } 420 421 public EventInfoFragment(Context context, Uri uri, long startMillis, long endMillis, 422 int attendeeResponse, boolean isDialog) { 423 424 if (mScale == 0) { 425 mScale = context.getResources().getDisplayMetrics().density; 426 if (mScale != 1) { 427 DIALOG_WIDTH *= mScale; 428 DIALOG_HEIGHT *= mScale; 429 } 430 } 431 mIsDialog = isDialog; 432 433 setStyle(DialogFragment.STYLE_NO_TITLE, 0); 434 mUri = uri; 435 mStartMillis = startMillis; 436 mEndMillis = endMillis; 437 mAttendeeResponseFromIntent = attendeeResponse; 438 } 439 440 // This is currently required by the fragment manager. 441 public EventInfoFragment() { 442 } 443 444 445 446 public EventInfoFragment(Context context, long eventId, long startMillis, long endMillis, 447 int attendeeResponse, boolean isDialog) { 448 this(context, ContentUris.withAppendedId(Events.CONTENT_URI, eventId), startMillis, 449 endMillis, attendeeResponse, isDialog); 450 mEventId = eventId; 451 } 452 453 @Override 454 public void onActivityCreated(Bundle savedInstanceState) { 455 super.onActivityCreated(savedInstanceState); 456 457 if (savedInstanceState != null) { 458 mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false); 459 } 460 461 if (mIsDialog) { 462 applyDialogParams(); 463 } 464 } 465 466 private void applyDialogParams() { 467 Dialog dialog = getDialog(); 468 dialog.setCanceledOnTouchOutside(true); 469 470 Window window = dialog.getWindow(); 471 window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 472 473 WindowManager.LayoutParams a = window.getAttributes(); 474 a.dimAmount = .4f; 475 476 a.width = DIALOG_WIDTH; 477 a.height = DIALOG_HEIGHT; 478 479 480 // On tablets , do smart positioning of dialog 481 // On phones , use the whole screen 482 483 if (mX != -1 || mY != -1) { 484 a.x = mX - a.width - 64; 485 if (a.x < 0) { 486 a.x = mX + 64; 487 } 488 a.y = mY - 64; 489 a.gravity = Gravity.LEFT | Gravity.TOP; 490 } 491 window.setAttributes(a); 492 } 493 494 public void setDialogParams(int x, int y) { 495 mX = x; 496 mY = y; 497 } 498 499 // Implements OnCheckedChangeListener 500 @Override 501 public void onCheckedChanged(RadioGroup group, int checkedId) { 502 // If this is not a repeating event, then don't display the dialog 503 // asking which events to change. 504 if (!mIsRepeating) { 505 return; 506 } 507 508 // If the selection is the same as the original, then don't display the 509 // dialog asking which events to change. 510 if (checkedId == findButtonIdForResponse(mOriginalAttendeeResponse)) { 511 return; 512 } 513 514 // This is a repeating event. We need to ask the user if they mean to 515 // change just this one instance or all instances. 516 mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents()); 517 } 518 519 public void onNothingSelected(AdapterView<?> parent) { 520 } 521 522 @Override 523 public void onAttach(Activity activity) { 524 super.onAttach(activity); 525 mActivity = activity; 526 mEditResponseHelper = new EditResponseHelper(activity); 527 mHandler = new QueryHandler(activity); 528 mDescLineNum = activity.getResources().getInteger((R.integer.event_info_desc_line_num)); 529 mMoreLabel = activity.getResources().getString((R.string.event_info_desc_more)); 530 mLessLabel = activity.getResources().getString((R.string.event_info_desc_less)); 531 if (!mIsDialog) { 532 setHasOptionsMenu(true); 533 } 534 } 535 536 @Override 537 public View onCreateView(LayoutInflater inflater, ViewGroup container, 538 Bundle savedInstanceState) { 539 mView = inflater.inflate(R.layout.event_info, container, false); 540 mScrollView = (ScrollView) mView.findViewById(R.id.event_info_scroll_view); 541 mTitle = (TextView) mView.findViewById(R.id.title); 542 mWhen = (TextView) mView.findViewById(R.id.when); 543 mWhere = (TextView) mView.findViewById(R.id.where); 544 mWhat = (TextView) mView.findViewById(R.id.description); 545 mAttendees = (TextView) mView.findViewById(R.id.attendee_list); 546 mHeadlines = mView.findViewById(R.id.event_info_headline); 547 mLongAttendees = (AttendeesView)mView.findViewById(R.id.long_attendee_list); 548 mDescButton = (Button)mView.findViewById(R.id.desc_expand); 549 mDescButton.setOnClickListener(new View.OnClickListener() { 550 @Override 551 public void onClick(View v) { 552 mShowMaxDescription = !mShowMaxDescription; 553 updateDescription(); 554 } 555 }); 556 mShowMaxDescription = false; // Show short version of description as default. 557 mIsTabletConfig = Utils.getConfigBool(mActivity, R.bool.tablet_config); 558 559 if (mUri == null) { 560 // restore event ID from bundle 561 mEventId = savedInstanceState.getLong(BUNDLE_KEY_EVENT_ID); 562 mUri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 563 mStartMillis = savedInstanceState.getLong(BUNDLE_KEY_START_MILLIS); 564 mEndMillis = savedInstanceState.getLong(BUNDLE_KEY_END_MILLIS); 565 } 566 567 // start loading the data 568 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 569 null, null, null); 570 571 Button b = (Button) mView.findViewById(R.id.delete); 572 b.setOnClickListener(new OnClickListener() { 573 @Override 574 public void onClick(View v) { 575 if (!mCanModifyCalendar) { 576 return; 577 } 578 DeleteEventHelper deleteHelper = new DeleteEventHelper( 579 getActivity(), getActivity(), 580 !mIsDialog && !mIsTabletConfig /* exitWhenDone */); 581 deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); 582 }}); 583 584 // Hide Edit/Delete buttons if in full screen mode on a phone 585 if (savedInstanceState != null) { 586 mIsDialog = savedInstanceState.getBoolean(BUNDLE_KEY_IS_DIALOG, false); 587 } 588 if (!mIsDialog && !mIsTabletConfig) { 589 mView.findViewById(R.id.event_info_buttons_container).setVisibility(View.GONE); 590 } 591 592 // Create a listener for the add reminder button 593 594 ImageButton reminderAddButton = (ImageButton) mView.findViewById(R.id.reminder_add); 595 View.OnClickListener addReminderOnClickListener = new View.OnClickListener() { 596 @Override 597 public void onClick(View v) { 598 addReminder(); 599 } 600 }; 601 reminderAddButton.setOnClickListener(addReminderOnClickListener); 602 603 // Set reminders variables 604 605 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mActivity); 606 String defaultReminderString = prefs.getString( 607 GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING); 608 mDefaultReminderMinutes = Integer.parseInt(defaultReminderString); 609 prepareReminders(); 610 611 return mView; 612 } 613 614 private Runnable onDeleteRunnable = new Runnable() { 615 @Override 616 public void run() { 617 if (EventInfoFragment.this.mIsPaused) { 618 mDismissOnResume = true; 619 return; 620 } 621 if (EventInfoFragment.this.isVisible()) { 622 EventInfoFragment.this.dismiss(); 623 } 624 } 625 }; 626 627 // Sets the description: 628 // Set the expand/collapse button 629 // Expand/collapse the description according the the current status 630 private void updateDescription() { 631 // Description is short, hide button 632 if (mWhat.getLineCount() <= mDescLineNum) { 633 mDescButton.setVisibility(View.GONE); 634 return; 635 } 636 // Show button and set label according to the expand/collapse status 637 mDescButton.setVisibility(View.VISIBLE); 638 if (mShowMaxDescription) { 639 mDescButton.setText(mLessLabel); 640 mWhat.setLines(mWhat.getLineCount()); 641 } else { 642 mDescButton.setText(mMoreLabel); 643 mWhat.setLines(mDescLineNum); 644 } 645 } 646 647 private void updateTitle() { 648 Resources res = getActivity().getResources(); 649 if (mCanModifyCalendar && !mIsOrganizer) { 650 getActivity().setTitle(res.getString(R.string.event_info_title_invite)); 651 } else { 652 getActivity().setTitle(res.getString(R.string.event_info_title)); 653 } 654 } 655 656 /** 657 * Initializes the event cursor, which is expected to point to the first 658 * (and only) result from a query. 659 * @return true if the cursor is empty. 660 */ 661 private boolean initEventCursor() { 662 if ((mEventCursor == null) || (mEventCursor.getCount() == 0)) { 663 return true; 664 } 665 mEventCursor.moveToFirst(); 666 mEventId = mEventCursor.getInt(EVENT_INDEX_ID); 667 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 668 mIsRepeating = !TextUtils.isEmpty(rRule); 669 mHasAlarm = (mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) == 1)?true:false; 670 mMaxReminders = mEventCursor.getInt(EVENT_INDEX_MAX_REMINDERS); 671 mCalendarAllowedReminders = mEventCursor.getString(EVENT_INDEX_ALLOWED_REMINDERS); 672 return false; 673 } 674 675 @SuppressWarnings("fallthrough") 676 private void initAttendeesCursor(View view) { 677 mOriginalAttendeeResponse = CalendarController.ATTENDEE_NO_RESPONSE; 678 mCalendarOwnerAttendeeId = EditEventHelper.ATTENDEE_ID_NONE; 679 mNumOfAttendees = 0; 680 if (mAttendeesCursor != null) { 681 mNumOfAttendees = mAttendeesCursor.getCount(); 682 if (mAttendeesCursor.moveToFirst()) { 683 mAcceptedAttendees.clear(); 684 mDeclinedAttendees.clear(); 685 mTentativeAttendees.clear(); 686 mNoResponseAttendees.clear(); 687 688 do { 689 int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 690 String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME); 691 String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL); 692 693 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE && 694 mCalendarOwnerAccount.equalsIgnoreCase(email)) { 695 mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID); 696 mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS); 697 } else { 698 // Don't show your own status in the list because: 699 // 1) it doesn't make sense for event without other guests. 700 // 2) there's a spinner for that for events with guests. 701 switch(status) { 702 case Attendees.ATTENDEE_STATUS_ACCEPTED: 703 mAcceptedAttendees.add(new Attendee(name, email, 704 Attendees.ATTENDEE_STATUS_ACCEPTED)); 705 break; 706 case Attendees.ATTENDEE_STATUS_DECLINED: 707 mDeclinedAttendees.add(new Attendee(name, email, 708 Attendees.ATTENDEE_STATUS_DECLINED)); 709 break; 710 case Attendees.ATTENDEE_STATUS_TENTATIVE: 711 mTentativeAttendees.add(new Attendee(name, email, 712 Attendees.ATTENDEE_STATUS_TENTATIVE)); 713 break; 714 default: 715 mNoResponseAttendees.add(new Attendee(name, email, 716 Attendees.ATTENDEE_STATUS_NONE)); 717 } 718 } 719 } while (mAttendeesCursor.moveToNext()); 720 mAttendeesCursor.moveToFirst(); 721 722 updateAttendees(view); 723 } 724 } 725 } 726 727 @Override 728 public void onSaveInstanceState(Bundle outState) { 729 super.onSaveInstanceState(outState); 730 outState.putLong(BUNDLE_KEY_EVENT_ID, mEventId); 731 outState.putLong(BUNDLE_KEY_START_MILLIS, mStartMillis); 732 outState.putLong(BUNDLE_KEY_END_MILLIS, mEndMillis); 733 outState.putBoolean(BUNDLE_KEY_IS_DIALOG, mIsDialog); 734 outState.putInt(BUNDLE_KEY_ATTENDEE_RESPONSE, mAttendeeResponseFromIntent); 735 } 736 737 738 @Override 739 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 740 super.onCreateOptionsMenu(menu, inflater); 741 // Show edit/delete buttons only in non-dialog configuration on a phone 742 if (!mIsDialog && !mIsTabletConfig) { 743 inflater.inflate(R.menu.event_info_title_bar, menu); 744 mMenu = menu; 745 updateMenu(); 746 } 747 } 748 749 @Override 750 public boolean onOptionsItemSelected(MenuItem item) { 751 752 // If we're a dialog or part of a tablet display we don't want to handle 753 // menu buttons 754 if (mIsDialog || mIsTabletConfig) { 755 return false; 756 } 757 // Handles option menu selections: 758 // Home button - close event info activity and start the main calendar 759 // one 760 // Edit button - start the event edit activity and close the info 761 // activity 762 // Delete button - start a delete query that calls a runnable that close 763 // the info activity 764 765 switch (item.getItemId()) { 766 case android.R.id.home: 767 Intent launchIntent = new Intent(); 768 launchIntent.setAction(Intent.ACTION_VIEW); 769 launchIntent.setData(Uri.parse(CalendarContract.CONTENT_URI + "/time")); 770 launchIntent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 771 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 772 launchIntent.setClass(mActivity, AllInOneActivity.class); 773 startActivity(launchIntent); 774 mActivity.finish(); 775 return true; 776 case R.id.info_action_edit: 777 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 778 Intent intent = new Intent(Intent.ACTION_EDIT, uri); 779 intent.putExtra(EXTRA_EVENT_BEGIN_TIME, mStartMillis); 780 intent.putExtra(EXTRA_EVENT_END_TIME, mEndMillis); 781 intent.setClass(mActivity, EditEventActivity.class); 782 intent.putExtra(EVENT_EDIT_ON_LAUNCH, true); 783 startActivity(intent); 784 mActivity.finish(); 785 break; 786 case R.id.info_action_delete: 787 DeleteEventHelper deleteHelper = 788 new DeleteEventHelper(mActivity, mActivity, true /* exitWhenDone */); 789 deleteHelper.delete(mStartMillis, mEndMillis, mEventId, -1, onDeleteRunnable); 790 break; 791 default: 792 break; 793 } 794 return super.onOptionsItemSelected(item); 795 } 796 797 @Override 798 public void onDestroyView() { 799 if (saveResponse() || saveReminders()) { 800 Toast.makeText(getActivity(), R.string.saving_event, Toast.LENGTH_SHORT).show(); 801 } 802 super.onDestroyView(); 803 } 804 805 @Override 806 public void onDestroy() { 807 if (mEventCursor != null) { 808 mEventCursor.close(); 809 } 810 if (mCalendarsCursor != null) { 811 mCalendarsCursor.close(); 812 } 813 if (mAttendeesCursor != null) { 814 mAttendeesCursor.close(); 815 } 816 super.onDestroy(); 817 } 818 819 /** 820 * Asynchronously saves the response to an invitation if the user changed 821 * the response. Returns true if the database will be updated. 822 * 823 * @return true if the database will be changed 824 */ 825 private boolean saveResponse() { 826 if (mAttendeesCursor == null || mEventCursor == null) { 827 return false; 828 } 829 830 RadioGroup radioGroup = (RadioGroup) getView().findViewById(R.id.response_value); 831 int status = getResponseFromButtonId(radioGroup.getCheckedRadioButtonId()); 832 if (status == Attendees.ATTENDEE_STATUS_NONE) { 833 return false; 834 } 835 836 // If the status has not changed, then don't update the database 837 if (status == mOriginalAttendeeResponse) { 838 return false; 839 } 840 841 // If we never got an owner attendee id we can't set the status 842 if (mCalendarOwnerAttendeeId == EditEventHelper.ATTENDEE_ID_NONE) { 843 return false; 844 } 845 846 if (!mIsRepeating) { 847 // This is a non-repeating event 848 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 849 return true; 850 } 851 852 // This is a repeating event 853 int whichEvents = mEditResponseHelper.getWhichEvents(); 854 switch (whichEvents) { 855 case -1: 856 return false; 857 case UPDATE_SINGLE: 858 createExceptionResponse(mEventId, status); 859 return true; 860 case UPDATE_ALL: 861 updateResponse(mEventId, mCalendarOwnerAttendeeId, status); 862 return true; 863 default: 864 Log.e(TAG, "Unexpected choice for updating invitation response"); 865 break; 866 } 867 return false; 868 } 869 870 private void updateResponse(long eventId, long attendeeId, int status) { 871 // Update the attendee status in the attendees table. the provider 872 // takes care of updating the self attendance status. 873 ContentValues values = new ContentValues(); 874 875 if (!TextUtils.isEmpty(mCalendarOwnerAccount)) { 876 values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount); 877 } 878 values.put(Attendees.ATTENDEE_STATUS, status); 879 values.put(Attendees.EVENT_ID, eventId); 880 881 Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId); 882 883 mHandler.startUpdate(mHandler.getNextToken(), null, uri, values, 884 null, null, Utils.UNDO_DELAY); 885 } 886 887 /** 888 * Creates an exception to a recurring event. The only change we're making is to the 889 * "self attendee status" value. The provider will take care of updating the corresponding 890 * Attendees.attendeeStatus entry. 891 * 892 * @param eventId The recurring event. 893 * @param status The new value for selfAttendeeStatus. 894 */ 895 private void createExceptionResponse(long eventId, int status) { 896 ContentValues values = new ContentValues(); 897 values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis); 898 values.put(Events.SELF_ATTENDEE_STATUS, status); 899 values.put(Events.STATUS, Events.STATUS_CONFIRMED); 900 901 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 902 Uri exceptionUri = Uri.withAppendedPath(Events.CONTENT_EXCEPTION_URI, 903 String.valueOf(eventId)); 904 ops.add(ContentProviderOperation.newInsert(exceptionUri).withValues(values).build()); 905 906 mHandler.startBatch(mHandler.getNextToken(), null, CalendarContract.AUTHORITY, ops, 907 Utils.UNDO_DELAY); 908 } 909 910 public static int getResponseFromButtonId(int buttonId) { 911 int response; 912 switch (buttonId) { 913 case R.id.response_yes: 914 response = Attendees.ATTENDEE_STATUS_ACCEPTED; 915 break; 916 case R.id.response_maybe: 917 response = Attendees.ATTENDEE_STATUS_TENTATIVE; 918 break; 919 case R.id.response_no: 920 response = Attendees.ATTENDEE_STATUS_DECLINED; 921 break; 922 default: 923 response = Attendees.ATTENDEE_STATUS_NONE; 924 } 925 return response; 926 } 927 928 public static int findButtonIdForResponse(int response) { 929 int buttonId; 930 switch (response) { 931 case Attendees.ATTENDEE_STATUS_ACCEPTED: 932 buttonId = R.id.response_yes; 933 break; 934 case Attendees.ATTENDEE_STATUS_TENTATIVE: 935 buttonId = R.id.response_maybe; 936 break; 937 case Attendees.ATTENDEE_STATUS_DECLINED: 938 buttonId = R.id.response_no; 939 break; 940 default: 941 buttonId = -1; 942 } 943 return buttonId; 944 } 945 946 private void doEdit() { 947 Context c = getActivity(); 948 // This ensures that we aren't in the process of closing and have been 949 // unattached already 950 if (c != null) { 951 CalendarController.getInstance(c).sendEventRelatedEvent( 952 this, EventType.VIEW_EVENT_DETAILS, mEventId, mStartMillis, mEndMillis, 0 953 , 0, -1); 954 } 955 } 956 957 private void updateEvent(View view) { 958 if (mEventCursor == null || view == null) { 959 return; 960 } 961 962 String eventName = mEventCursor.getString(EVENT_INDEX_TITLE); 963 if (eventName == null || eventName.length() == 0) { 964 eventName = getActivity().getString(R.string.no_title_label); 965 } 966 967 boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0; 968 String location = mEventCursor.getString(EVENT_INDEX_EVENT_LOCATION); 969 String description = mEventCursor.getString(EVENT_INDEX_DESCRIPTION); 970 String rRule = mEventCursor.getString(EVENT_INDEX_RRULE); 971 String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE); 972 String organizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER); 973 974 mColor = Utils.getDisplayColorFromColor(mEventCursor.getInt(EVENT_INDEX_COLOR)); 975 mHeadlines.setBackgroundColor(mColor); 976 977 // What 978 if (eventName != null) { 979 setTextCommon(view, R.id.title, eventName); 980 } 981 982 // When 983 // Set the date and repeats (if any) 984 String whenDate; 985 int flagsTime = DateUtils.FORMAT_SHOW_TIME; 986 int flagsDate = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY | 987 DateUtils.FORMAT_SHOW_YEAR; 988 989 if (DateFormat.is24HourFormat(getActivity())) { 990 flagsTime |= DateUtils.FORMAT_24HOUR; 991 } 992 993 // Put repeat after the date (if any) 994 String repeatString = null; 995 if (!TextUtils.isEmpty(rRule)) { 996 EventRecurrence eventRecurrence = new EventRecurrence(); 997 eventRecurrence.parse(rRule); 998 Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater)); 999 if (allDay) { 1000 date.timezone = Time.TIMEZONE_UTC; 1001 } 1002 date.set(mStartMillis); 1003 eventRecurrence.setStartDate(date); 1004 repeatString = EventRecurrenceFormatter.getRepeatString( 1005 getActivity().getResources(), eventRecurrence); 1006 } 1007 // If an all day event , show the date without the time 1008 if (allDay) { 1009 Formatter f = new Formatter(new StringBuilder(50), Locale.getDefault()); 1010 whenDate = DateUtils.formatDateRange(getActivity(), f, mStartMillis, mStartMillis, 1011 flagsDate, Time.TIMEZONE_UTC).toString(); 1012 if (repeatString != null) { 1013 setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")"); 1014 } else { 1015 setTextCommon(view, R.id.when_date, whenDate); 1016 } 1017 view.findViewById(R.id.when_time).setVisibility(View.GONE); 1018 1019 } else { 1020 // Show date for none all-day events 1021 whenDate = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, flagsDate); 1022 String whenTime = Utils.formatDateRange(getActivity(), mStartMillis, mEndMillis, 1023 flagsTime); 1024 if (repeatString != null) { 1025 setTextCommon(view, R.id.when_date, whenDate + " (" + repeatString + ")"); 1026 } else { 1027 setTextCommon(view, R.id.when_date, whenDate); 1028 } 1029 1030 // Show the event timezone if it is different from the local timezone after the time 1031 // TODO: Fix comparison of Timezone 1032 String localTimezone = Utils.getTimeZone(mActivity, mTZUpdater); 1033 if (!TextUtils.equals(localTimezone, eventTimezone)) { 1034 String displayName; 1035 // Figure out if this is in DST 1036 Time date = new Time(Utils.getTimeZone(getActivity(), mTZUpdater)); 1037 if (allDay) { 1038 date.timezone = Time.TIMEZONE_UTC; 1039 } 1040 date.set(mStartMillis); 1041 1042 TimeZone tz = TimeZone.getTimeZone(localTimezone); 1043 if (tz == null || tz.getID().equals("GMT")) { 1044 displayName = localTimezone; 1045 } else { 1046 displayName = tz.getDisplayName(date.isDst != 0, TimeZone.LONG); 1047 } 1048 setTextCommon(view, R.id.when_time, whenTime + " (" + displayName + ")"); 1049 } 1050 else { 1051 setTextCommon(view, R.id.when_time, whenTime); 1052 } 1053 } 1054 1055 1056 // Organizer view is setup in the updateCalendar method 1057 1058 1059 // Where 1060 if (location == null || location.trim().length() == 0) { 1061 setVisibilityCommon(view, R.id.where, View.GONE); 1062 } else { 1063 final TextView textView = mWhere; 1064 if (textView != null) { 1065 textView.setAutoLinkMask(0); 1066 textView.setText(location.trim()); 1067 if (!Linkify.addLinks(textView, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES 1068 | Linkify.MAP_ADDRESSES)) { 1069 Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q="); 1070 } 1071 textView.setOnTouchListener(new OnTouchListener() { 1072 @Override 1073 public boolean onTouch(View v, MotionEvent event) { 1074 try { 1075 return v.onTouchEvent(event); 1076 } catch (ActivityNotFoundException e) { 1077 // ignore 1078 return true; 1079 } 1080 } 1081 }); 1082 } 1083 } 1084 1085 // Description 1086 if (description != null && description.length() != 0) { 1087 setTextCommon(view, R.id.description, description); 1088 } 1089 updateDescription (); // Expand or collapse full description 1090 } 1091 1092 private void sendAccessibilityEvent() { 1093 AccessibilityManager am = 1094 (AccessibilityManager) getActivity().getSystemService(Service.ACCESSIBILITY_SERVICE); 1095 if (!am.isEnabled()) { 1096 return; 1097 } 1098 1099 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED); 1100 event.setClassName(getClass().getName()); 1101 event.setPackageName(getActivity().getPackageName()); 1102 List<CharSequence> text = event.getText(); 1103 1104 addFieldToAccessibilityEvent(text, mTitle); 1105 addFieldToAccessibilityEvent(text, mWhen); 1106 addFieldToAccessibilityEvent(text, mWhere); 1107 addFieldToAccessibilityEvent(text, mWhat); 1108 addFieldToAccessibilityEvent(text, mAttendees); 1109 1110 RadioGroup response = (RadioGroup) getView().findViewById(R.id.response_value); 1111 if (response.getVisibility() == View.VISIBLE) { 1112 int id = response.getCheckedRadioButtonId(); 1113 if (id != View.NO_ID) { 1114 text.add(((TextView) getView().findViewById(R.id.response_label)).getText()); 1115 text.add((((RadioButton) (response.findViewById(id))).getText() + PERIOD_SPACE)); 1116 } 1117 } 1118 1119 am.sendAccessibilityEvent(event); 1120 } 1121 1122 /** 1123 * @param text 1124 */ 1125 private void addFieldToAccessibilityEvent(List<CharSequence> text, TextView view) { 1126 if (view == null) { 1127 return; 1128 } 1129 String str = view.toString().trim(); 1130 if (!TextUtils.isEmpty(str)) { 1131 text.add(mTitle.getText()); 1132 text.add(PERIOD_SPACE); 1133 } 1134 } 1135 1136 private void updateCalendar(View view) { 1137 mCalendarOwnerAccount = ""; 1138 if (mCalendarsCursor != null && mEventCursor != null) { 1139 mCalendarsCursor.moveToFirst(); 1140 String tempAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT); 1141 mCalendarOwnerAccount = (tempAccount == null) ? "" : tempAccount; 1142 mOwnerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0; 1143 1144 String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME); 1145 1146 // start duplicate calendars query 1147 mHandler.startQuery(TOKEN_QUERY_DUPLICATE_CALENDARS, null, Calendars.CONTENT_URI, 1148 CALENDARS_PROJECTION, CALENDARS_DUPLICATE_NAME_WHERE, 1149 new String[] {displayName}, null); 1150 1151 String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER); 1152 mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer); 1153 setTextCommon(view, R.id.organizer, eventOrganizer); 1154 if (!mIsOrganizer) { 1155 setVisibilityCommon(view, R.id.organizer_container, View.VISIBLE); 1156 } else { 1157 setVisibilityCommon(view, R.id.organizer_container, View.GONE); 1158 } 1159 mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0; 1160 mCanModifyCalendar = 1161 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CAL_ACCESS_CONTRIBUTOR; 1162 mIsBusyFreeCalendar = 1163 mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.CAL_ACCESS_FREEBUSY; 1164 1165 if (!mIsBusyFreeCalendar) { 1166 Button b = (Button) mView.findViewById(R.id.edit); 1167 b.setEnabled(true); 1168 b.setOnClickListener(new OnClickListener() { 1169 @Override 1170 public void onClick(View v) { 1171 doEdit(); 1172 // For dialogs, just close the fragment 1173 // For full screen, close activity on phone, leave it for tablet 1174 if (mIsDialog) { 1175 EventInfoFragment.this.dismiss(); 1176 } 1177 else if (!mIsTabletConfig){ 1178 getActivity().finish(); 1179 } 1180 } 1181 }); 1182 } 1183 if (!mCanModifyCalendar) { 1184 if (mIsDialog) { 1185 mView.findViewById(R.id.delete).setEnabled(false); 1186 } 1187 } 1188 if (mMenu != null) { 1189 mActivity.invalidateOptionsMenu(); 1190 } 1191 } else { 1192 setVisibilityCommon(view, R.id.calendar, View.GONE); 1193 sendAccessibilityEventIfQueryDone(TOKEN_QUERY_DUPLICATE_CALENDARS); 1194 } 1195 } 1196 1197 /** 1198 * 1199 */ 1200 private void updateMenu() { 1201 if (mMenu == null) { 1202 return; 1203 } 1204 MenuItem delete = mMenu.findItem(R.id.info_action_delete); 1205 MenuItem edit = mMenu.findItem(R.id.info_action_edit); 1206 if (delete != null) { 1207 delete.setVisible(mCanModifyCalendar); 1208 delete.setEnabled(mCanModifyCalendar); 1209 } 1210 if (edit != null) { 1211 edit.setVisible(mCanModifyCalendar); 1212 edit.setEnabled(mCanModifyCalendar); 1213 } 1214 } 1215 1216 private void updateAttendees(View view) { 1217 1218 if (mAcceptedAttendees.size() + mDeclinedAttendees.size() + 1219 mTentativeAttendees.size() + mNoResponseAttendees.size() > 0) { 1220 (mLongAttendees).addAttendees(mAcceptedAttendees); 1221 (mLongAttendees).addAttendees(mDeclinedAttendees); 1222 (mLongAttendees).addAttendees(mTentativeAttendees); 1223 (mLongAttendees).addAttendees(mNoResponseAttendees); 1224 mLongAttendees.setEnabled(false); 1225 mLongAttendees.setVisibility(View.VISIBLE); 1226 } else { 1227 mLongAttendees.setVisibility(View.GONE); 1228 } 1229 } 1230 1231 public void initReminders(View view, Cursor cursor) { 1232 1233 // Add reminders 1234 while (cursor.moveToNext()) { 1235 int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES); 1236 int method = cursor.getInt(EditEventHelper.REMINDERS_INDEX_METHOD); 1237 mOriginalReminders.add(ReminderEntry.valueOf(minutes, method)); 1238 } 1239 // Sort appropriately for display (by time, then type) 1240 Collections.sort(mOriginalReminders); 1241 1242 // Load the labels and corresponding numeric values for the minutes and methods lists 1243 // from the assets. If we're switching calendars, we need to clear and re-populate the 1244 // lists (which may have elements added and removed based on calendar properties). This 1245 // is mostly relevant for "methods", since we shouldn't have any "minutes" values in a 1246 // new event that aren't in the default set. 1247 Resources r = mActivity.getResources(); 1248 mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values); 1249 mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels); 1250 mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values); 1251 mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels); 1252 1253 // Remove any reminder methods that aren't allowed for this calendar. If this is 1254 // a new event, mCalendarAllowedReminders may not be set the first time we're called. 1255 if (mCalendarAllowedReminders != null) { 1256 EventViewUtils.reduceMethodList(mReminderMethodValues, mReminderMethodLabels, 1257 mCalendarAllowedReminders); 1258 } 1259 1260 int numReminders = 0; 1261 if (mHasAlarm) { 1262 ArrayList<ReminderEntry> reminders = mOriginalReminders; 1263 numReminders = reminders.size(); 1264 // Insert any minute values that aren't represented in the minutes list. 1265 for (ReminderEntry re : reminders) { 1266 EventViewUtils.addMinutesToList( 1267 mActivity, mReminderMinuteValues, mReminderMinuteLabels, re.getMinutes()); 1268 } 1269 // Create a UI element for each reminder. We display all of the reminders we get 1270 // from the provider, even if the count exceeds the calendar maximum. (Also, for 1271 // a new event, we won't have a maxReminders value available.) 1272 for (ReminderEntry re : reminders) { 1273 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1274 mReminderMinuteValues, mReminderMinuteLabels, 1275 mReminderMethodValues, mReminderMethodLabels, 1276 re, Integer.MAX_VALUE); 1277 } 1278 } 1279 } 1280 1281 private void formatAttendees(ArrayList<Attendee> attendees, SpannableStringBuilder sb, int type) { 1282 if (attendees.size() <= 0) { 1283 return; 1284 } 1285 1286 int begin = sb.length(); 1287 boolean firstTime = sb.length() == 0; 1288 1289 if (firstTime == false) { 1290 begin += 2; // skip over the ", " for formatting. 1291 } 1292 1293 for (Attendee attendee : attendees) { 1294 if (firstTime) { 1295 firstTime = false; 1296 } else { 1297 sb.append(", "); 1298 } 1299 1300 String name = attendee.getDisplayName(); 1301 sb.append(name); 1302 } 1303 1304 switch (type) { 1305 case Attendees.ATTENDEE_STATUS_ACCEPTED: 1306 break; 1307 case Attendees.ATTENDEE_STATUS_DECLINED: 1308 sb.setSpan(new StrikethroughSpan(), begin, sb.length(), 1309 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1310 // fall through 1311 default: 1312 // The last INCLUSIVE causes the foreground color to be applied 1313 // to the rest of the span. If not, the comma at the end of the 1314 // declined or tentative may be black. 1315 sb.setSpan(new ForegroundColorSpan(0xFF999999), begin, sb.length(), 1316 Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 1317 break; 1318 } 1319 } 1320 1321 void updateResponse(View view) { 1322 // we only let the user accept/reject/etc. a meeting if: 1323 // a) you can edit the event's containing calendar AND 1324 // b) you're not the organizer and only attendee AND 1325 // c) organizerCanRespond is enabled for the calendar 1326 // (if the attendee data has been hidden, the visible number of attendees 1327 // will be 1 -- the calendar owner's). 1328 // (there are more cases involved to be 100% accurate, such as 1329 // paying attention to whether or not an attendee status was 1330 // included in the feed, but we're currently omitting those corner cases 1331 // for simplicity). 1332 1333 // TODO Switch to EditEventHelper.canRespond when this class uses CalendarEventModel. 1334 if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) || 1335 (mIsOrganizer && !mOwnerCanRespond)) { 1336 setVisibilityCommon(view, R.id.response_container, View.GONE); 1337 return; 1338 } 1339 1340 setVisibilityCommon(view, R.id.response_container, View.VISIBLE); 1341 1342 1343 int response; 1344 if (mAttendeeResponseFromIntent != CalendarController.ATTENDEE_NO_RESPONSE) { 1345 response = mAttendeeResponseFromIntent; 1346 } else { 1347 response = mOriginalAttendeeResponse; 1348 } 1349 1350 int buttonToCheck = findButtonIdForResponse(response); 1351 RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.response_value); 1352 radioGroup.check(buttonToCheck); // -1 clear all radio buttons 1353 radioGroup.setOnCheckedChangeListener(this); 1354 } 1355 1356 private void setTextCommon(View view, int id, CharSequence text) { 1357 TextView textView = (TextView) view.findViewById(id); 1358 if (textView == null) 1359 return; 1360 textView.setText(text); 1361 } 1362 1363 private void setVisibilityCommon(View view, int id, int visibility) { 1364 View v = view.findViewById(id); 1365 if (v != null) { 1366 v.setVisibility(visibility); 1367 } 1368 return; 1369 } 1370 1371 /** 1372 * Taken from com.google.android.gm.HtmlConversationActivity 1373 * 1374 * Send the intent that shows the Contact info corresponding to the email address. 1375 */ 1376 public void showContactInfo(Attendee attendee, Rect rect) { 1377 // First perform lookup query to find existing contact 1378 final ContentResolver resolver = getActivity().getContentResolver(); 1379 final String address = attendee.mEmail; 1380 final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI, 1381 Uri.encode(address)); 1382 final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri); 1383 1384 if (lookupUri != null) { 1385 // Found matching contact, trigger QuickContact 1386 QuickContact.showQuickContact(getActivity(), rect, lookupUri, 1387 QuickContact.MODE_MEDIUM, null); 1388 } else { 1389 // No matching contact, ask user to create one 1390 final Uri mailUri = Uri.fromParts("mailto", address, null); 1391 final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri); 1392 1393 // Pass along full E-mail string for possible create dialog 1394 Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null); 1395 intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString()); 1396 1397 // Only provide personal name hint if we have one 1398 final String senderPersonal = attendee.mName; 1399 if (!TextUtils.isEmpty(senderPersonal)) { 1400 intent.putExtra(Intents.Insert.NAME, senderPersonal); 1401 } 1402 1403 startActivity(intent); 1404 } 1405 } 1406 1407 @Override 1408 public void onPause() { 1409 mIsPaused = true; 1410 mHandler.removeCallbacks(onDeleteRunnable); 1411 super.onPause(); 1412 } 1413 1414 @Override 1415 public void onResume() { 1416 super.onResume(); 1417 mIsPaused = false; 1418 if (mDismissOnResume) { 1419 mHandler.post(onDeleteRunnable); 1420 } 1421 } 1422 1423 @Override 1424 public void eventsChanged() { 1425 } 1426 1427 @Override 1428 public long getSupportedEventTypes() { 1429 return EventType.EVENTS_CHANGED; 1430 } 1431 1432 @Override 1433 public void handleEvent(EventInfo event) { 1434 if (event.eventType == EventType.EVENTS_CHANGED) { 1435 // reload the data 1436 mHandler.startQuery(TOKEN_QUERY_EVENT, null, mUri, EVENT_PROJECTION, 1437 null, null, null); 1438 } 1439 1440 } 1441 1442 1443 @Override 1444 public void onClick(View view) { 1445 1446 // This must be a click on one of the "remove reminder" buttons 1447 LinearLayout reminderItem = (LinearLayout) view.getParent(); 1448 LinearLayout parent = (LinearLayout) reminderItem.getParent(); 1449 parent.removeView(reminderItem); 1450 mReminderViews.remove(reminderItem); 1451 } 1452 1453 1454 /** 1455 * Add a new reminder when the user hits the "add reminder" button. We use the default 1456 * reminder time and method. 1457 */ 1458 private void addReminder() { 1459 // TODO: when adding a new reminder, make it different from the 1460 // last one in the list (if any). 1461 if (mDefaultReminderMinutes == GeneralPreferences.NO_REMINDER) { 1462 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1463 mReminderMinuteValues, mReminderMinuteLabels, 1464 mReminderMethodValues, mReminderMethodLabels, 1465 ReminderEntry.valueOf(GeneralPreferences.REMINDER_DEFAULT_TIME), 1466 mMaxReminders); 1467 } else { 1468 EventViewUtils.addReminder(mActivity, mScrollView, this, mReminderViews, 1469 mReminderMinuteValues, mReminderMinuteLabels, 1470 mReminderMethodValues, mReminderMethodLabels, 1471 ReminderEntry.valueOf(mDefaultReminderMinutes), 1472 mMaxReminders); 1473 } 1474 } 1475 1476 1477 private void prepareReminders() { 1478 Resources r = mActivity.getResources(); 1479 mReminderMinuteValues = loadIntegerArray(r, R.array.reminder_minutes_values); 1480 mReminderMinuteLabels = loadStringArray(r, R.array.reminder_minutes_labels); 1481 mReminderMethodValues = loadIntegerArray(r, R.array.reminder_methods_values); 1482 mReminderMethodLabels = loadStringArray(r, R.array.reminder_methods_labels); 1483 } 1484 1485 1486 private boolean saveReminders() { 1487 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3); 1488 1489 // Read reminders from UI 1490 mReminders = EventViewUtils.reminderItemsToReminders(mReminderViews, 1491 mReminderMinuteValues, mReminderMethodValues); 1492 1493 // Check if there are any changes in the reminder 1494 boolean changed = EditEventHelper.saveReminders(ops, mEventId, mReminders, 1495 mOriginalReminders, false /* no force save */); 1496 1497 if (!changed) { 1498 return false; 1499 } 1500 1501 // save new reminders 1502 AsyncQueryService service = new AsyncQueryService(getActivity()); 1503 service.startBatch(0, null, Calendars.CONTENT_URI.getAuthority(), ops, 0); 1504 // Update the "hasAlarm" field for the event 1505 Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId); 1506 int len = mReminders.size(); 1507 boolean hasAlarm = len > 0; 1508 if (hasAlarm != mHasAlarm) { 1509 ContentValues values = new ContentValues(); 1510 values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0); 1511 service.startUpdate(0, null, uri, values, null, null, 0); 1512 } 1513 return true; 1514 } 1515 1516 /** 1517 * Loads an integer array asset into a list. 1518 */ 1519 private static ArrayList<Integer> loadIntegerArray(Resources r, int resNum) { 1520 int[] vals = r.getIntArray(resNum); 1521 int size = vals.length; 1522 ArrayList<Integer> list = new ArrayList<Integer>(size); 1523 1524 for (int i = 0; i < size; i++) { 1525 list.add(vals[i]); 1526 } 1527 1528 return list; 1529 } 1530 /** 1531 * Loads a String array asset into a list. 1532 */ 1533 private static ArrayList<String> loadStringArray(Resources r, int resNum) { 1534 String[] labels = r.getStringArray(resNum); 1535 ArrayList<String> list = new ArrayList<String>(Arrays.asList(labels)); 1536 return list; 1537 } 1538 1539} 1540