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