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