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.contacts.common.list; 18 19 import android.content.Context; 20 import android.content.res.ColorStateList; 21 import android.content.res.TypedArray; 22 import android.database.CharArrayBuffer; 23 import android.database.Cursor; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.Rect; 27 import android.graphics.Typeface; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.provider.ContactsContract; 31 import android.provider.ContactsContract.Contacts; 32 import android.text.Spannable; 33 import android.text.SpannableString; 34 import android.text.TextUtils; 35 import android.text.TextUtils.TruncateAt; 36 import android.util.AttributeSet; 37 import android.util.TypedValue; 38 import android.view.Gravity; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.widget.AbsListView.SelectionBoundsAdjuster; 43 import android.widget.ImageView; 44 import android.widget.ImageView.ScaleType; 45 import android.widget.QuickContactBadge; 46 import android.widget.TextView; 47 48 import com.android.contacts.common.ContactPresenceIconUtil; 49 import com.android.contacts.common.ContactStatusUtil; 50 import com.android.contacts.common.R; 51 import com.android.contacts.common.format.TextHighlighter; 52 import com.android.contacts.common.util.ContactDisplayUtils; 53 import com.android.contacts.common.util.SearchUtil; 54 import com.android.contacts.common.util.ViewUtil; 55 56 import com.google.common.collect.Lists; 57 58 import java.util.ArrayList; 59 import java.util.List; 60 import java.util.Locale; 61 import java.util.regex.Matcher; 62 import java.util.regex.Pattern; 63 64 /** 65 * A custom view for an item in the contact list. 66 * The view contains the contact's photo, a set of text views (for name, status, etc...) and 67 * icons for presence and call. 68 * The view uses no XML file for layout and all the measurements and layouts are done 69 * in the onMeasure and onLayout methods. 70 * 71 * The layout puts the contact's photo on the right side of the view, the call icon (if present) 72 * to the left of the photo, the text lines are aligned to the left and the presence icon (if 73 * present) is set to the left of the status line. 74 * 75 * The layout also supports a header (used as a header of a group of contacts) that is above the 76 * contact's data and a divider between contact view. 77 */ 78 79 public class ContactListItemView extends ViewGroup 80 implements SelectionBoundsAdjuster { 81 82 // Style values for layout and appearance 83 // The initialized values are defaults if none is provided through xml. 84 private int mPreferredHeight = 0; 85 private int mGapBetweenImageAndText = 0; 86 private int mGapBetweenLabelAndData = 0; 87 private int mPresenceIconMargin = 4; 88 private int mPresenceIconSize = 16; 89 private int mTextIndent = 0; 90 private int mTextOffsetTop; 91 private int mNameTextViewTextSize; 92 private int mHeaderWidth; 93 private Drawable mActivatedBackgroundDrawable; 94 95 // Set in onLayout. Represent left and right position of the View on the screen. 96 private int mLeftOffset; 97 private int mRightOffset; 98 99 /** 100 * Used with {@link #mLabelView}, specifying the width ratio between label and data. 101 */ 102 private int mLabelViewWidthWeight = 3; 103 /** 104 * Used with {@link #mDataView}, specifying the width ratio between label and data. 105 */ 106 private int mDataViewWidthWeight = 5; 107 108 protected static class HighlightSequence { 109 private final int start; 110 private final int end; 111 HighlightSequence(int start, int end)112 HighlightSequence(int start, int end) { 113 this.start = start; 114 this.end = end; 115 } 116 } 117 118 private ArrayList<HighlightSequence> mNameHighlightSequence; 119 private ArrayList<HighlightSequence> mNumberHighlightSequence; 120 121 // Highlighting prefix for names. 122 private String mHighlightedPrefix; 123 124 /** 125 * Where to put contact photo. This affects the other Views' layout or look-and-feel. 126 * 127 * TODO: replace enum with int constants 128 */ 129 public enum PhotoPosition { 130 LEFT, 131 RIGHT 132 } 133 getDefaultPhotoPosition(boolean opposite)134 static public final PhotoPosition getDefaultPhotoPosition(boolean opposite) { 135 final Locale locale = Locale.getDefault(); 136 final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); 137 switch (layoutDirection) { 138 case View.LAYOUT_DIRECTION_RTL: 139 return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT); 140 case View.LAYOUT_DIRECTION_LTR: 141 default: 142 return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT); 143 } 144 } 145 146 private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */); 147 148 // Header layout data 149 private TextView mHeaderTextView; 150 private boolean mIsSectionHeaderEnabled; 151 152 // The views inside the contact view 153 private boolean mQuickContactEnabled = true; 154 private QuickContactBadge mQuickContact; 155 private ImageView mPhotoView; 156 private TextView mNameTextView; 157 private TextView mPhoneticNameTextView; 158 private TextView mLabelView; 159 private TextView mDataView; 160 private TextView mSnippetView; 161 private TextView mStatusView; 162 private ImageView mPresenceIcon; 163 164 private ColorStateList mSecondaryTextColor; 165 166 167 168 private int mDefaultPhotoViewSize = 0; 169 /** 170 * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding 171 * to align other data in this View. 172 */ 173 private int mPhotoViewWidth; 174 /** 175 * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding. 176 */ 177 private int mPhotoViewHeight; 178 179 /** 180 * Only effective when {@link #mPhotoView} is null. 181 * When true all the Views on the right side of the photo should have horizontal padding on 182 * those left assuming there is a photo. 183 */ 184 private boolean mKeepHorizontalPaddingForPhotoView; 185 /** 186 * Only effective when {@link #mPhotoView} is null. 187 */ 188 private boolean mKeepVerticalPaddingForPhotoView; 189 190 /** 191 * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used. 192 * False indicates those values should be updated before being used in position calculation. 193 */ 194 private boolean mPhotoViewWidthAndHeightAreReady = false; 195 196 private int mNameTextViewHeight; 197 private int mNameTextViewTextColor = Color.BLACK; 198 private int mPhoneticNameTextViewHeight; 199 private int mLabelViewHeight; 200 private int mDataViewHeight; 201 private int mSnippetTextViewHeight; 202 private int mStatusTextViewHeight; 203 204 // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the 205 // same row. 206 private int mLabelAndDataViewMaxHeight; 207 208 // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is 209 // more efficient for each case or in general, and simplify the whole implementation. 210 // Note: if we're sure MARQUEE will be used every time, there's no reason to use 211 // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the 212 // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to 213 // TextView without any modification. 214 private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128); 215 private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128); 216 217 private boolean mActivatedStateSupported; 218 private boolean mAdjustSelectionBoundsEnabled = true; 219 220 private Rect mBoundsWithoutHeader = new Rect(); 221 222 /** A helper used to highlight a prefix in a text field. */ 223 private final TextHighlighter mTextHighlighter; 224 private CharSequence mUnknownNameText; 225 ContactListItemView(Context context)226 public ContactListItemView(Context context) { 227 super(context); 228 229 mTextHighlighter = new TextHighlighter(Typeface.BOLD); 230 mNameHighlightSequence = new ArrayList<HighlightSequence>(); 231 mNumberHighlightSequence = new ArrayList<HighlightSequence>(); 232 } 233 ContactListItemView(Context context, AttributeSet attrs)234 public ContactListItemView(Context context, AttributeSet attrs) { 235 super(context, attrs); 236 237 // Read all style values 238 TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); 239 mPreferredHeight = a.getDimensionPixelSize( 240 R.styleable.ContactListItemView_list_item_height, mPreferredHeight); 241 mActivatedBackgroundDrawable = a.getDrawable( 242 R.styleable.ContactListItemView_activated_background); 243 244 mGapBetweenImageAndText = a.getDimensionPixelOffset( 245 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 246 mGapBetweenImageAndText); 247 mGapBetweenLabelAndData = a.getDimensionPixelOffset( 248 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 249 mGapBetweenLabelAndData); 250 mPresenceIconMargin = a.getDimensionPixelOffset( 251 R.styleable.ContactListItemView_list_item_presence_icon_margin, 252 mPresenceIconMargin); 253 mPresenceIconSize = a.getDimensionPixelOffset( 254 R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize); 255 mDefaultPhotoViewSize = a.getDimensionPixelOffset( 256 R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize); 257 mTextIndent = a.getDimensionPixelOffset( 258 R.styleable.ContactListItemView_list_item_text_indent, mTextIndent); 259 mTextOffsetTop = a.getDimensionPixelOffset( 260 R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop); 261 mDataViewWidthWeight = a.getInteger( 262 R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight); 263 mLabelViewWidthWeight = a.getInteger( 264 R.styleable.ContactListItemView_list_item_label_width_weight, 265 mLabelViewWidthWeight); 266 mNameTextViewTextColor = a.getColor( 267 R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor); 268 mNameTextViewTextSize = (int) a.getDimension( 269 R.styleable.ContactListItemView_list_item_name_text_size, 270 (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size)); 271 272 setPaddingRelative( 273 a.getDimensionPixelOffset( 274 R.styleable.ContactListItemView_list_item_padding_left, 0), 275 a.getDimensionPixelOffset( 276 R.styleable.ContactListItemView_list_item_padding_top, 0), 277 a.getDimensionPixelOffset( 278 R.styleable.ContactListItemView_list_item_padding_right, 0), 279 a.getDimensionPixelOffset( 280 R.styleable.ContactListItemView_list_item_padding_bottom, 0)); 281 282 mTextHighlighter = new TextHighlighter(Typeface.BOLD); 283 284 a.recycle(); 285 286 a = getContext().obtainStyledAttributes(R.styleable.Theme); 287 mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary); 288 a.recycle(); 289 290 mHeaderWidth = 291 getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); 292 293 if (mActivatedBackgroundDrawable != null) { 294 mActivatedBackgroundDrawable.setCallback(this); 295 } 296 297 mNameHighlightSequence = new ArrayList<HighlightSequence>(); 298 mNumberHighlightSequence = new ArrayList<HighlightSequence>(); 299 300 setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); 301 } 302 setUnknownNameText(CharSequence unknownNameText)303 public void setUnknownNameText(CharSequence unknownNameText) { 304 mUnknownNameText = unknownNameText; 305 } 306 setQuickContactEnabled(boolean flag)307 public void setQuickContactEnabled(boolean flag) { 308 mQuickContactEnabled = flag; 309 } 310 311 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)312 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 313 // We will match parent's width and wrap content vertically, but make sure 314 // height is no less than listPreferredItemHeight. 315 final int specWidth = resolveSize(0, widthMeasureSpec); 316 final int preferredHeight = mPreferredHeight; 317 318 mNameTextViewHeight = 0; 319 mPhoneticNameTextViewHeight = 0; 320 mLabelViewHeight = 0; 321 mDataViewHeight = 0; 322 mLabelAndDataViewMaxHeight = 0; 323 mSnippetTextViewHeight = 0; 324 mStatusTextViewHeight = 0; 325 326 ensurePhotoViewSize(); 327 328 // Width each TextView is able to use. 329 int effectiveWidth; 330 // All the other Views will honor the photo, so available width for them may be shrunk. 331 if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) { 332 effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight() 333 - (mPhotoViewWidth + mGapBetweenImageAndText); 334 } else { 335 effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight(); 336 } 337 338 if (mIsSectionHeaderEnabled) { 339 effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText; 340 } 341 342 // Go over all visible text views and measure actual width of each of them. 343 // Also calculate their heights to get the total height for this entire view. 344 345 if (isVisible(mNameTextView)) { 346 // Calculate width for name text - this parallels similar measurement in onLayout. 347 int nameTextWidth = effectiveWidth; 348 if (mPhotoPosition != PhotoPosition.LEFT) { 349 nameTextWidth -= mTextIndent; 350 } 351 mNameTextView.measure( 352 MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY), 353 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 354 mNameTextViewHeight = mNameTextView.getMeasuredHeight(); 355 } 356 357 if (isVisible(mPhoneticNameTextView)) { 358 mPhoneticNameTextView.measure( 359 MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), 360 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 361 mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight(); 362 } 363 364 // If both data (phone number/email address) and label (type like "MOBILE") are quite long, 365 // we should ellipsize both using appropriate ratio. 366 final int dataWidth; 367 final int labelWidth; 368 if (isVisible(mDataView)) { 369 if (isVisible(mLabelView)) { 370 final int totalWidth = effectiveWidth - mGapBetweenLabelAndData; 371 dataWidth = ((totalWidth * mDataViewWidthWeight) 372 / (mDataViewWidthWeight + mLabelViewWidthWeight)); 373 labelWidth = ((totalWidth * mLabelViewWidthWeight) / 374 (mDataViewWidthWeight + mLabelViewWidthWeight)); 375 } else { 376 dataWidth = effectiveWidth; 377 labelWidth = 0; 378 } 379 } else { 380 dataWidth = 0; 381 if (isVisible(mLabelView)) { 382 labelWidth = effectiveWidth; 383 } else { 384 labelWidth = 0; 385 } 386 } 387 388 if (isVisible(mDataView)) { 389 mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY), 390 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 391 mDataViewHeight = mDataView.getMeasuredHeight(); 392 } 393 394 if (isVisible(mLabelView)) { 395 // For performance reason we don't want AT_MOST usually, but when the picture is 396 // on right, we need to use it anyway because mDataView is next to mLabelView. 397 final int mode = (mPhotoPosition == PhotoPosition.LEFT 398 ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); 399 mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, mode), 400 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 401 mLabelViewHeight = mLabelView.getMeasuredHeight(); 402 } 403 mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight); 404 405 if (isVisible(mSnippetView)) { 406 mSnippetView.measure( 407 MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), 408 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 409 mSnippetTextViewHeight = mSnippetView.getMeasuredHeight(); 410 } 411 412 // Status view height is the biggest of the text view and the presence icon 413 if (isVisible(mPresenceIcon)) { 414 mPresenceIcon.measure( 415 MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY), 416 MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY)); 417 mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight(); 418 } 419 420 if (isVisible(mStatusView)) { 421 // Presence and status are in a same row, so status will be affected by icon size. 422 final int statusWidth; 423 if (isVisible(mPresenceIcon)) { 424 statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() 425 - mPresenceIconMargin); 426 } else { 427 statusWidth = effectiveWidth; 428 } 429 mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY), 430 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 431 mStatusTextViewHeight = 432 Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight()); 433 } 434 435 // Calculate height including padding. 436 int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight + 437 mLabelAndDataViewMaxHeight + 438 mSnippetTextViewHeight + mStatusTextViewHeight); 439 440 // Make sure the height is at least as high as the photo 441 height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop()); 442 443 // Make sure height is at least the preferred height 444 height = Math.max(height, preferredHeight); 445 446 // Measure the header if it is visible. 447 if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) { 448 mHeaderTextView.measure( 449 MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY), 450 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 451 } 452 453 setMeasuredDimension(specWidth, height); 454 } 455 456 @Override onLayout(boolean changed, int left, int top, int right, int bottom)457 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 458 final int height = bottom - top; 459 final int width = right - left; 460 461 // Determine the vertical bounds by laying out the header first. 462 int topBound = 0; 463 int bottomBound = height; 464 int leftBound = getPaddingLeft(); 465 int rightBound = width - getPaddingRight(); 466 467 final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this); 468 469 // Put the section header on the left side of the contact view. 470 if (mIsSectionHeaderEnabled) { 471 if (mHeaderTextView != null) { 472 int headerHeight = mHeaderTextView.getMeasuredHeight(); 473 int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop; 474 475 mHeaderTextView.layout( 476 isLayoutRtl ? rightBound - mHeaderWidth : leftBound, 477 headerTopBound, 478 isLayoutRtl ? rightBound : leftBound + mHeaderWidth, 479 headerTopBound + headerHeight); 480 } 481 if (isLayoutRtl) { 482 rightBound -= mHeaderWidth; 483 } else { 484 leftBound += mHeaderWidth; 485 } 486 } 487 488 mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound); 489 mLeftOffset = left + leftBound; 490 mRightOffset = left + rightBound; 491 if (mIsSectionHeaderEnabled) { 492 if (isLayoutRtl) { 493 rightBound -= mGapBetweenImageAndText; 494 } else { 495 leftBound += mGapBetweenImageAndText; 496 } 497 } 498 499 if (mActivatedStateSupported && isActivated()) { 500 mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); 501 } 502 503 final View photoView = mQuickContact != null ? mQuickContact : mPhotoView; 504 if (mPhotoPosition == PhotoPosition.LEFT) { 505 // Photo is the left most view. All the other Views should on the right of the photo. 506 if (photoView != null) { 507 // Center the photo vertically 508 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; 509 photoView.layout( 510 leftBound, 511 photoTop, 512 leftBound + mPhotoViewWidth, 513 photoTop + mPhotoViewHeight); 514 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 515 } else if (mKeepHorizontalPaddingForPhotoView) { 516 // Draw nothing but keep the padding. 517 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 518 } 519 } else { 520 // Photo is the right most view. Right bound should be adjusted that way. 521 if (photoView != null) { 522 // Center the photo vertically 523 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; 524 photoView.layout( 525 rightBound - mPhotoViewWidth, 526 photoTop, 527 rightBound, 528 photoTop + mPhotoViewHeight); 529 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 530 } else if (mKeepHorizontalPaddingForPhotoView) { 531 // Draw nothing but keep the padding. 532 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 533 } 534 535 // Add indent between left-most padding and texts. 536 leftBound += mTextIndent; 537 } 538 539 // Center text vertically, then apply the top offset. 540 final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight + 541 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight; 542 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop; 543 544 // Layout all text view and presence icon 545 // Put name TextView first 546 if (isVisible(mNameTextView)) { 547 mNameTextView.layout(leftBound, 548 textTopBound, 549 rightBound, 550 textTopBound + mNameTextViewHeight); 551 textTopBound += mNameTextViewHeight; 552 } 553 554 // Presence and status 555 if (isLayoutRtl) { 556 int statusRightBound = rightBound; 557 if (isVisible(mPresenceIcon)) { 558 int iconWidth = mPresenceIcon.getMeasuredWidth(); 559 mPresenceIcon.layout( 560 rightBound - iconWidth, 561 textTopBound, 562 rightBound, 563 textTopBound + mStatusTextViewHeight); 564 statusRightBound -= (iconWidth + mPresenceIconMargin); 565 } 566 567 if (isVisible(mStatusView)) { 568 mStatusView.layout(leftBound, 569 textTopBound, 570 statusRightBound, 571 textTopBound + mStatusTextViewHeight); 572 } 573 } else { 574 int statusLeftBound = leftBound; 575 if (isVisible(mPresenceIcon)) { 576 int iconWidth = mPresenceIcon.getMeasuredWidth(); 577 mPresenceIcon.layout( 578 leftBound, 579 textTopBound, 580 leftBound + iconWidth, 581 textTopBound + mStatusTextViewHeight); 582 statusLeftBound += (iconWidth + mPresenceIconMargin); 583 } 584 585 if (isVisible(mStatusView)) { 586 mStatusView.layout(statusLeftBound, 587 textTopBound, 588 rightBound, 589 textTopBound + mStatusTextViewHeight); 590 } 591 } 592 593 if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { 594 textTopBound += mStatusTextViewHeight; 595 } 596 597 // Rest of text views 598 int dataLeftBound = leftBound; 599 if (isVisible(mPhoneticNameTextView)) { 600 mPhoneticNameTextView.layout(leftBound, 601 textTopBound, 602 rightBound, 603 textTopBound + mPhoneticNameTextViewHeight); 604 textTopBound += mPhoneticNameTextViewHeight; 605 } 606 607 // Label and Data align bottom. 608 if (isVisible(mLabelView)) { 609 if (mPhotoPosition == PhotoPosition.LEFT) { 610 // When photo is on left, label is placed on the right edge of the list item. 611 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(), 612 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, 613 rightBound, 614 textTopBound + mLabelAndDataViewMaxHeight); 615 rightBound -= mLabelView.getMeasuredWidth(); 616 } else { 617 // When photo is on right, label is placed on the left of data view. 618 dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); 619 mLabelView.layout(leftBound, 620 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, 621 dataLeftBound, 622 textTopBound + mLabelAndDataViewMaxHeight); 623 dataLeftBound += mGapBetweenLabelAndData; 624 } 625 } 626 627 if (isVisible(mDataView)) { 628 mDataView.layout(dataLeftBound, 629 textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, 630 rightBound, 631 textTopBound + mLabelAndDataViewMaxHeight); 632 } 633 if (isVisible(mLabelView) || isVisible(mDataView)) { 634 textTopBound += mLabelAndDataViewMaxHeight; 635 } 636 637 if (isVisible(mSnippetView)) { 638 mSnippetView.layout(leftBound, 639 textTopBound, 640 rightBound, 641 textTopBound + mSnippetTextViewHeight); 642 } 643 } 644 645 @Override adjustListItemSelectionBounds(Rect bounds)646 public void adjustListItemSelectionBounds(Rect bounds) { 647 if (mAdjustSelectionBoundsEnabled) { 648 bounds.top += mBoundsWithoutHeader.top; 649 bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); 650 bounds.left = mBoundsWithoutHeader.left; 651 bounds.right = mBoundsWithoutHeader.right; 652 } 653 } 654 isVisible(View view)655 protected boolean isVisible(View view) { 656 return view != null && view.getVisibility() == View.VISIBLE; 657 } 658 659 /** 660 * Extracts width and height from the style 661 */ ensurePhotoViewSize()662 private void ensurePhotoViewSize() { 663 if (!mPhotoViewWidthAndHeightAreReady) { 664 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); 665 if (!mQuickContactEnabled && mPhotoView == null) { 666 if (!mKeepHorizontalPaddingForPhotoView) { 667 mPhotoViewWidth = 0; 668 } 669 if (!mKeepVerticalPaddingForPhotoView) { 670 mPhotoViewHeight = 0; 671 } 672 } 673 674 mPhotoViewWidthAndHeightAreReady = true; 675 } 676 } 677 getDefaultPhotoViewSize()678 protected int getDefaultPhotoViewSize() { 679 return mDefaultPhotoViewSize; 680 } 681 682 /** 683 * Gets a LayoutParam that corresponds to the default photo size. 684 * 685 * @return A new LayoutParam. 686 */ getDefaultPhotoLayoutParams()687 private LayoutParams getDefaultPhotoLayoutParams() { 688 LayoutParams params = generateDefaultLayoutParams(); 689 params.width = getDefaultPhotoViewSize(); 690 params.height = params.width; 691 return params; 692 } 693 694 @Override drawableStateChanged()695 protected void drawableStateChanged() { 696 super.drawableStateChanged(); 697 if (mActivatedStateSupported) { 698 mActivatedBackgroundDrawable.setState(getDrawableState()); 699 } 700 } 701 702 @Override verifyDrawable(Drawable who)703 protected boolean verifyDrawable(Drawable who) { 704 return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); 705 } 706 707 @Override jumpDrawablesToCurrentState()708 public void jumpDrawablesToCurrentState() { 709 super.jumpDrawablesToCurrentState(); 710 if (mActivatedStateSupported) { 711 mActivatedBackgroundDrawable.jumpToCurrentState(); 712 } 713 } 714 715 @Override dispatchDraw(Canvas canvas)716 public void dispatchDraw(Canvas canvas) { 717 if (mActivatedStateSupported && isActivated()) { 718 mActivatedBackgroundDrawable.draw(canvas); 719 } 720 721 super.dispatchDraw(canvas); 722 } 723 724 /** 725 * Sets section header or makes it invisible if the title is null. 726 */ setSectionHeader(String title)727 public void setSectionHeader(String title) { 728 if (!TextUtils.isEmpty(title)) { 729 if (mHeaderTextView == null) { 730 mHeaderTextView = new TextView(getContext()); 731 mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle); 732 mHeaderTextView.setGravity( 733 ViewUtil.isViewLayoutRtl(this) ? Gravity.RIGHT : Gravity.LEFT); 734 addView(mHeaderTextView); 735 } 736 setMarqueeText(mHeaderTextView, title); 737 mHeaderTextView.setVisibility(View.VISIBLE); 738 mHeaderTextView.setAllCaps(true); 739 } else if (mHeaderTextView != null) { 740 mHeaderTextView.setVisibility(View.GONE); 741 } 742 } 743 setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled)744 public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) { 745 mIsSectionHeaderEnabled = isSectionHeaderEnabled; 746 } 747 748 /** 749 * Returns the quick contact badge, creating it if necessary. 750 */ getQuickContact()751 public QuickContactBadge getQuickContact() { 752 if (!mQuickContactEnabled) { 753 throw new IllegalStateException("QuickContact is disabled for this view"); 754 } 755 if (mQuickContact == null) { 756 mQuickContact = new QuickContactBadge(getContext()); 757 mQuickContact.setOverlay(null); 758 mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams()); 759 if (mNameTextView != null) { 760 mQuickContact.setContentDescription(getContext().getString( 761 R.string.description_quick_contact_for, mNameTextView.getText())); 762 } 763 764 addView(mQuickContact); 765 mPhotoViewWidthAndHeightAreReady = false; 766 } 767 return mQuickContact; 768 } 769 770 /** 771 * Returns the photo view, creating it if necessary. 772 */ getPhotoView()773 public ImageView getPhotoView() { 774 if (mPhotoView == null) { 775 mPhotoView = new ImageView(getContext()); 776 mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams()); 777 // Quick contact style used above will set a background - remove it 778 mPhotoView.setBackground(null); 779 addView(mPhotoView); 780 mPhotoViewWidthAndHeightAreReady = false; 781 } 782 return mPhotoView; 783 } 784 785 /** 786 * Removes the photo view. 787 */ removePhotoView()788 public void removePhotoView() { 789 removePhotoView(false, true); 790 } 791 792 /** 793 * Removes the photo view. 794 * 795 * @param keepHorizontalPadding True means data on the right side will have 796 * padding on left, pretending there is still a photo view. 797 * @param keepVerticalPadding True means the View will have some height 798 * enough for accommodating a photo view. 799 */ removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)800 public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { 801 mPhotoViewWidthAndHeightAreReady = false; 802 mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; 803 mKeepVerticalPaddingForPhotoView = keepVerticalPadding; 804 if (mPhotoView != null) { 805 removeView(mPhotoView); 806 mPhotoView = null; 807 } 808 if (mQuickContact != null) { 809 removeView(mQuickContact); 810 mQuickContact = null; 811 } 812 } 813 814 /** 815 * Sets a word prefix that will be highlighted if encountered in fields like 816 * name and search snippet. This will disable the mask highlighting for names. 817 * <p> 818 * NOTE: must be all upper-case 819 */ setHighlightedPrefix(String upperCasePrefix)820 public void setHighlightedPrefix(String upperCasePrefix) { 821 mHighlightedPrefix = upperCasePrefix; 822 } 823 824 /** 825 * Clears previously set highlight sequences for the view. 826 */ clearHighlightSequences()827 public void clearHighlightSequences() { 828 mNameHighlightSequence.clear(); 829 mNumberHighlightSequence.clear(); 830 mHighlightedPrefix = null; 831 } 832 833 /** 834 * Adds a highlight sequence to the name highlighter. 835 * @param start The start position of the highlight sequence. 836 * @param end The end position of the highlight sequence. 837 */ addNameHighlightSequence(int start, int end)838 public void addNameHighlightSequence(int start, int end) { 839 mNameHighlightSequence.add(new HighlightSequence(start, end)); 840 } 841 842 /** 843 * Adds a highlight sequence to the number highlighter. 844 * @param start The start position of the highlight sequence. 845 * @param end The end position of the highlight sequence. 846 */ addNumberHighlightSequence(int start, int end)847 public void addNumberHighlightSequence(int start, int end) { 848 mNumberHighlightSequence.add(new HighlightSequence(start, end)); 849 } 850 851 /** 852 * Returns the text view for the contact name, creating it if necessary. 853 */ getNameTextView()854 public TextView getNameTextView() { 855 if (mNameTextView == null) { 856 mNameTextView = new TextView(getContext()); 857 mNameTextView.setSingleLine(true); 858 mNameTextView.setEllipsize(getTextEllipsis()); 859 mNameTextView.setTextColor(mNameTextViewTextColor); 860 mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, 861 mNameTextViewTextSize); 862 // Manually call setActivated() since this view may be added after the first 863 // setActivated() call toward this whole item view. 864 mNameTextView.setActivated(isActivated()); 865 mNameTextView.setGravity(Gravity.CENTER_VERTICAL); 866 mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 867 mNameTextView.setId(R.id.cliv_name_textview); 868 mNameTextView.setElegantTextHeight(false); 869 addView(mNameTextView); 870 } 871 return mNameTextView; 872 } 873 874 /** 875 * Adds or updates a text view for the phonetic name. 876 */ setPhoneticName(char[] text, int size)877 public void setPhoneticName(char[] text, int size) { 878 if (text == null || size == 0) { 879 if (mPhoneticNameTextView != null) { 880 mPhoneticNameTextView.setVisibility(View.GONE); 881 } 882 } else { 883 getPhoneticNameTextView(); 884 setMarqueeText(mPhoneticNameTextView, text, size); 885 mPhoneticNameTextView.setVisibility(VISIBLE); 886 } 887 } 888 889 /** 890 * Returns the text view for the phonetic name, creating it if necessary. 891 */ getPhoneticNameTextView()892 public TextView getPhoneticNameTextView() { 893 if (mPhoneticNameTextView == null) { 894 mPhoneticNameTextView = new TextView(getContext()); 895 mPhoneticNameTextView.setSingleLine(true); 896 mPhoneticNameTextView.setEllipsize(getTextEllipsis()); 897 mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 898 mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 899 mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); 900 mPhoneticNameTextView.setActivated(isActivated()); 901 mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview); 902 addView(mPhoneticNameTextView); 903 } 904 return mPhoneticNameTextView; 905 } 906 907 /** 908 * Adds or updates a text view for the data label. 909 */ setLabel(CharSequence text)910 public void setLabel(CharSequence text) { 911 if (TextUtils.isEmpty(text)) { 912 if (mLabelView != null) { 913 mLabelView.setVisibility(View.GONE); 914 } 915 } else { 916 getLabelView(); 917 setMarqueeText(mLabelView, text); 918 mLabelView.setVisibility(VISIBLE); 919 } 920 } 921 922 /** 923 * Returns the text view for the data label, creating it if necessary. 924 */ getLabelView()925 public TextView getLabelView() { 926 if (mLabelView == null) { 927 mLabelView = new TextView(getContext()); 928 mLabelView.setSingleLine(true); 929 mLabelView.setEllipsize(getTextEllipsis()); 930 mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); 931 if (mPhotoPosition == PhotoPosition.LEFT) { 932 mLabelView.setAllCaps(true); 933 mLabelView.setGravity(Gravity.END); 934 } else { 935 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); 936 } 937 mLabelView.setActivated(isActivated()); 938 mLabelView.setId(R.id.cliv_label_textview); 939 addView(mLabelView); 940 } 941 return mLabelView; 942 } 943 944 /** 945 * Adds or updates a text view for the data element. 946 */ setData(char[] text, int size)947 public void setData(char[] text, int size) { 948 if (text == null || size == 0) { 949 if (mDataView != null) { 950 mDataView.setVisibility(View.GONE); 951 } 952 } else { 953 getDataView(); 954 setMarqueeText(mDataView, text, size); 955 mDataView.setVisibility(VISIBLE); 956 } 957 } 958 959 /** 960 * Sets phone number for a list item. This takes care of number highlighting if the highlight 961 * mask exists. 962 */ setPhoneNumber(String text, String countryIso)963 public void setPhoneNumber(String text, String countryIso) { 964 if (text == null) { 965 if (mDataView != null) { 966 mDataView.setVisibility(View.GONE); 967 } 968 } else { 969 getDataView(); 970 971 // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to 972 // mDataView. Make sure that determination of the highlight sequences are done only 973 // after number formatting. 974 975 // Sets phone number texts for display after highlighting it, if applicable. 976 // CharSequence textToSet = text; 977 final SpannableString textToSet = new SpannableString(text); 978 979 if (mNumberHighlightSequence.size() != 0) { 980 final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0); 981 mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start, 982 highlightSequence.end); 983 } 984 985 setMarqueeText(mDataView, textToSet); 986 mDataView.setVisibility(VISIBLE); 987 988 // We have a phone number as "mDataView" so make it always LTR and VIEW_START 989 mDataView.setTextDirection(View.TEXT_DIRECTION_LTR); 990 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 991 } 992 } 993 setMarqueeText(TextView textView, char[] text, int size)994 private void setMarqueeText(TextView textView, char[] text, int size) { 995 if (getTextEllipsis() == TruncateAt.MARQUEE) { 996 setMarqueeText(textView, new String(text, 0, size)); 997 } else { 998 textView.setText(text, 0, size); 999 } 1000 } 1001 setMarqueeText(TextView textView, CharSequence text)1002 private void setMarqueeText(TextView textView, CharSequence text) { 1003 if (getTextEllipsis() == TruncateAt.MARQUEE) { 1004 // To show MARQUEE correctly (with END effect during non-active state), we need 1005 // to build Spanned with MARQUEE in addition to TextView's ellipsize setting. 1006 final SpannableString spannable = new SpannableString(text); 1007 spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(), 1008 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1009 textView.setText(spannable); 1010 } else { 1011 textView.setText(text); 1012 } 1013 } 1014 1015 /** 1016 * Returns the text view for the data text, creating it if necessary. 1017 */ getDataView()1018 public TextView getDataView() { 1019 if (mDataView == null) { 1020 mDataView = new TextView(getContext()); 1021 mDataView.setSingleLine(true); 1022 mDataView.setEllipsize(getTextEllipsis()); 1023 mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); 1024 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1025 mDataView.setActivated(isActivated()); 1026 mDataView.setId(R.id.cliv_data_view); 1027 mDataView.setElegantTextHeight(false); 1028 addView(mDataView); 1029 } 1030 return mDataView; 1031 } 1032 1033 /** 1034 * Adds or updates a text view for the search snippet. 1035 */ setSnippet(String text)1036 public void setSnippet(String text) { 1037 if (TextUtils.isEmpty(text)) { 1038 if (mSnippetView != null) { 1039 mSnippetView.setVisibility(View.GONE); 1040 } 1041 } else { 1042 mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); 1043 mSnippetView.setVisibility(VISIBLE); 1044 if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { 1045 // Give the text-to-speech engine a hint that it's a phone number 1046 mSnippetView.setContentDescription( 1047 ContactDisplayUtils.getTelephoneTtsSpannable(text)); 1048 } else { 1049 mSnippetView.setContentDescription(null); 1050 } 1051 } 1052 } 1053 1054 /** 1055 * Returns the text view for the search snippet, creating it if necessary. 1056 */ getSnippetView()1057 public TextView getSnippetView() { 1058 if (mSnippetView == null) { 1059 mSnippetView = new TextView(getContext()); 1060 mSnippetView.setSingleLine(true); 1061 mSnippetView.setEllipsize(getTextEllipsis()); 1062 mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 1063 mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1064 mSnippetView.setActivated(isActivated()); 1065 addView(mSnippetView); 1066 } 1067 return mSnippetView; 1068 } 1069 1070 /** 1071 * Returns the text view for the status, creating it if necessary. 1072 */ getStatusView()1073 public TextView getStatusView() { 1074 if (mStatusView == null) { 1075 mStatusView = new TextView(getContext()); 1076 mStatusView.setSingleLine(true); 1077 mStatusView.setEllipsize(getTextEllipsis()); 1078 mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 1079 mStatusView.setTextColor(mSecondaryTextColor); 1080 mStatusView.setActivated(isActivated()); 1081 mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1082 addView(mStatusView); 1083 } 1084 return mStatusView; 1085 } 1086 1087 /** 1088 * Adds or updates a text view for the status. 1089 */ setStatus(CharSequence text)1090 public void setStatus(CharSequence text) { 1091 if (TextUtils.isEmpty(text)) { 1092 if (mStatusView != null) { 1093 mStatusView.setVisibility(View.GONE); 1094 } 1095 } else { 1096 getStatusView(); 1097 setMarqueeText(mStatusView, text); 1098 mStatusView.setVisibility(VISIBLE); 1099 } 1100 } 1101 1102 /** 1103 * Adds or updates the presence icon view. 1104 */ setPresence(Drawable icon)1105 public void setPresence(Drawable icon) { 1106 if (icon != null) { 1107 if (mPresenceIcon == null) { 1108 mPresenceIcon = new ImageView(getContext()); 1109 addView(mPresenceIcon); 1110 } 1111 mPresenceIcon.setImageDrawable(icon); 1112 mPresenceIcon.setScaleType(ScaleType.CENTER); 1113 mPresenceIcon.setVisibility(View.VISIBLE); 1114 } else { 1115 if (mPresenceIcon != null) { 1116 mPresenceIcon.setVisibility(View.GONE); 1117 } 1118 } 1119 } 1120 getTextEllipsis()1121 private TruncateAt getTextEllipsis() { 1122 return TruncateAt.MARQUEE; 1123 } 1124 showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)1125 public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) { 1126 CharSequence name = cursor.getString(nameColumnIndex); 1127 setDisplayName(name); 1128 1129 // Since the quick contact content description is derived from the display name and there is 1130 // no guarantee that when the quick contact is initialized the display name is already set, 1131 // do it here too. 1132 if (mQuickContact != null) { 1133 mQuickContact.setContentDescription(getContext().getString( 1134 R.string.description_quick_contact_for, mNameTextView.getText())); 1135 } 1136 } 1137 setDisplayName(CharSequence name, boolean highlight)1138 public void setDisplayName(CharSequence name, boolean highlight) { 1139 if (!TextUtils.isEmpty(name) && highlight) { 1140 clearHighlightSequences(); 1141 addNameHighlightSequence(0, name.length()); 1142 } 1143 setDisplayName(name); 1144 } 1145 setDisplayName(CharSequence name)1146 public void setDisplayName(CharSequence name) { 1147 if (!TextUtils.isEmpty(name)) { 1148 // Chooses the available highlighting method for highlighting. 1149 if (mHighlightedPrefix != null) { 1150 name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix); 1151 } else if (mNameHighlightSequence.size() != 0) { 1152 final SpannableString spannableName = new SpannableString(name); 1153 for (HighlightSequence highlightSequence : mNameHighlightSequence) { 1154 mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start, 1155 highlightSequence.end); 1156 } 1157 name = spannableName; 1158 } 1159 } else { 1160 name = mUnknownNameText; 1161 } 1162 setMarqueeText(getNameTextView(), name); 1163 1164 if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { 1165 // Give the text-to-speech engine a hint that it's a phone number 1166 mNameTextView.setContentDescription( 1167 ContactDisplayUtils.getTelephoneTtsSpannable(name.toString())); 1168 } else { 1169 mNameTextView.setContentDescription(null); 1170 } 1171 } 1172 hideDisplayName()1173 public void hideDisplayName() { 1174 if (mNameTextView != null) { 1175 removeView(mNameTextView); 1176 mNameTextView = null; 1177 } 1178 } 1179 showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)1180 public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) { 1181 cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer); 1182 int phoneticNameSize = mPhoneticNameBuffer.sizeCopied; 1183 if (phoneticNameSize != 0) { 1184 setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize); 1185 } else { 1186 setPhoneticName(null, 0); 1187 } 1188 } 1189 hidePhoneticName()1190 public void hidePhoneticName() { 1191 if (mPhoneticNameTextView != null) { 1192 removeView(mPhoneticNameTextView); 1193 mPhoneticNameTextView = null; 1194 } 1195 } 1196 1197 /** 1198 * Sets the proper icon (star or presence or nothing) and/or status message. 1199 */ showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)1200 public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, 1201 int contactStatusColumnIndex) { 1202 Drawable icon = null; 1203 int presence = 0; 1204 if (!cursor.isNull(presenceColumnIndex)) { 1205 presence = cursor.getInt(presenceColumnIndex); 1206 icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); 1207 } 1208 setPresence(icon); 1209 1210 String statusMessage = null; 1211 if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { 1212 statusMessage = cursor.getString(contactStatusColumnIndex); 1213 } 1214 // If there is no status message from the contact, but there was a presence value, then use 1215 // the default status message string 1216 if (statusMessage == null && presence != 0) { 1217 statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); 1218 } 1219 setStatus(statusMessage); 1220 } 1221 1222 /** 1223 * Shows search snippet. 1224 */ showSnippet(Cursor cursor, int summarySnippetColumnIndex)1225 public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { 1226 if (cursor.getColumnCount() <= summarySnippetColumnIndex) { 1227 setSnippet(null); 1228 return; 1229 } 1230 1231 String snippet = cursor.getString(summarySnippetColumnIndex); 1232 1233 // Do client side snippeting if provider didn't do it 1234 final Bundle extras = cursor.getExtras(); 1235 if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { 1236 1237 final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY); 1238 1239 String displayName = null; 1240 int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); 1241 if (displayNameIndex >= 0) { 1242 displayName = cursor.getString(displayNameIndex); 1243 } 1244 1245 snippet = updateSnippet(snippet, query, displayName); 1246 1247 } else { 1248 if (snippet != null) { 1249 int from = 0; 1250 int to = snippet.length(); 1251 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH); 1252 if (start == -1) { 1253 snippet = null; 1254 } else { 1255 int firstNl = snippet.lastIndexOf('\n', start); 1256 if (firstNl != -1) { 1257 from = firstNl + 1; 1258 } 1259 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH); 1260 if (end != -1) { 1261 int lastNl = snippet.indexOf('\n', end); 1262 if (lastNl != -1) { 1263 to = lastNl; 1264 } 1265 } 1266 1267 StringBuilder sb = new StringBuilder(); 1268 for (int i = from; i < to; i++) { 1269 char c = snippet.charAt(i); 1270 if (c != DefaultContactListAdapter.SNIPPET_START_MATCH && 1271 c != DefaultContactListAdapter.SNIPPET_END_MATCH) { 1272 sb.append(c); 1273 } 1274 } 1275 snippet = sb.toString(); 1276 } 1277 } 1278 } 1279 1280 setSnippet(snippet); 1281 } 1282 1283 /** 1284 * Used for deferred snippets from the database. The contents come back as large strings which 1285 * need to be extracted for display. 1286 * 1287 * @param snippet The snippet from the database. 1288 * @param query The search query substring. 1289 * @param displayName The contact display name. 1290 * @return The proper snippet to display. 1291 */ updateSnippet(String snippet, String query, String displayName)1292 private String updateSnippet(String snippet, String query, String displayName) { 1293 1294 if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) { 1295 return null; 1296 } 1297 query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase()); 1298 1299 // If the display name already contains the query term, return empty - snippets should 1300 // not be needed in that case. 1301 if (!TextUtils.isEmpty(displayName)) { 1302 final String lowerDisplayName = displayName.toLowerCase(); 1303 final List<String> nameTokens = split(lowerDisplayName); 1304 for (String nameToken : nameTokens) { 1305 if (nameToken.startsWith(query)) { 1306 return null; 1307 } 1308 } 1309 } 1310 1311 // The snippet may contain multiple data lines. 1312 // Show the first line that matches the query. 1313 final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query); 1314 1315 if (matched != null && matched.line != null) { 1316 // Tokenize for long strings since the match may be at the end of it. 1317 // Skip this part for short strings since the whole string will be displayed. 1318 // Most contact strings are short so the snippetize method will be called infrequently. 1319 final int lengthThreshold = getResources().getInteger( 1320 R.integer.snippet_length_before_tokenize); 1321 if (matched.line.length() > lengthThreshold) { 1322 return snippetize(matched.line, matched.startIndex, lengthThreshold); 1323 } else { 1324 return matched.line; 1325 } 1326 } 1327 1328 // No match found. 1329 return null; 1330 } 1331 snippetize(String line, int matchIndex, int maxLength)1332 private String snippetize(String line, int matchIndex, int maxLength) { 1333 // Show up to maxLength characters. But we only show full tokens so show the last full token 1334 // up to maxLength characters. So as many starting tokens as possible before trying ending 1335 // tokens. 1336 int remainingLength = maxLength; 1337 int tempRemainingLength = remainingLength; 1338 1339 // Start the end token after the matched query. 1340 int index = matchIndex; 1341 int endTokenIndex = index; 1342 1343 // Find the match token first. 1344 while (index < line.length()) { 1345 if (!Character.isLetterOrDigit(line.charAt(index))) { 1346 endTokenIndex = index; 1347 remainingLength = tempRemainingLength; 1348 break; 1349 } 1350 tempRemainingLength--; 1351 index++; 1352 } 1353 1354 // Find as much content before the match. 1355 index = matchIndex - 1; 1356 tempRemainingLength = remainingLength; 1357 int startTokenIndex = matchIndex; 1358 while (index > -1 && tempRemainingLength > 0) { 1359 if (!Character.isLetterOrDigit(line.charAt(index))) { 1360 startTokenIndex = index; 1361 remainingLength = tempRemainingLength; 1362 } 1363 tempRemainingLength--; 1364 index--; 1365 } 1366 1367 index = endTokenIndex; 1368 tempRemainingLength = remainingLength; 1369 // Find remaining content at after match. 1370 while (index < line.length() && tempRemainingLength > 0) { 1371 if (!Character.isLetterOrDigit(line.charAt(index))) { 1372 endTokenIndex = index; 1373 } 1374 tempRemainingLength--; 1375 index++; 1376 } 1377 // Append ellipse if there is content before or after. 1378 final StringBuilder sb = new StringBuilder(); 1379 if (startTokenIndex > 0) { 1380 sb.append("..."); 1381 } 1382 sb.append(line.substring(startTokenIndex, endTokenIndex)); 1383 if (endTokenIndex < line.length()) { 1384 sb.append("..."); 1385 } 1386 return sb.toString(); 1387 } 1388 1389 private static final Pattern SPLIT_PATTERN = Pattern.compile( 1390 "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); 1391 1392 /** 1393 * Helper method for splitting a string into tokens. The lists passed in are populated with 1394 * the 1395 * tokens and offsets into the content of each token. The tokenization function parses e-mail 1396 * addresses as a single token; otherwise it splits on any non-alphanumeric character. 1397 * 1398 * @param content Content to split. 1399 * @return List of token strings. 1400 */ split(String content)1401 private static List<String> split(String content) { 1402 final Matcher matcher = SPLIT_PATTERN.matcher(content); 1403 final ArrayList<String> tokens = Lists.newArrayList(); 1404 while (matcher.find()) { 1405 tokens.add(matcher.group()); 1406 } 1407 return tokens; 1408 } 1409 1410 /** 1411 * Shows data element. 1412 */ showData(Cursor cursor, int dataColumnIndex)1413 public void showData(Cursor cursor, int dataColumnIndex) { 1414 cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); 1415 setData(mDataBuffer.data, mDataBuffer.sizeCopied); 1416 } 1417 setActivatedStateSupported(boolean flag)1418 public void setActivatedStateSupported(boolean flag) { 1419 this.mActivatedStateSupported = flag; 1420 } 1421 setAdjustSelectionBoundsEnabled(boolean enabled)1422 public void setAdjustSelectionBoundsEnabled(boolean enabled) { 1423 mAdjustSelectionBoundsEnabled = enabled; 1424 } 1425 1426 @Override requestLayout()1427 public void requestLayout() { 1428 // We will assume that once measured this will not need to resize 1429 // itself, so there is no need to pass the layout request to the parent 1430 // view (ListView). 1431 forceLayout(); 1432 } 1433 setPhotoPosition(PhotoPosition photoPosition)1434 public void setPhotoPosition(PhotoPosition photoPosition) { 1435 mPhotoPosition = photoPosition; 1436 } 1437 getPhotoPosition()1438 public PhotoPosition getPhotoPosition() { 1439 return mPhotoPosition; 1440 } 1441 1442 /** 1443 * Set drawable resources directly for both the background and the drawable resource 1444 * of the photo view 1445 * 1446 * @param backgroundId Id of background resource 1447 * @param drawableId Id of drawable resource 1448 */ setDrawableResource(int backgroundId, int drawableId)1449 public void setDrawableResource(int backgroundId, int drawableId) { 1450 final ImageView photo = getPhotoView(); 1451 photo.setScaleType(ImageView.ScaleType.CENTER); 1452 photo.setBackgroundResource(backgroundId); 1453 photo.setImageResource(drawableId); 1454 } 1455 1456 @Override onTouchEvent(MotionEvent event)1457 public boolean onTouchEvent(MotionEvent event) { 1458 final float x = event.getX(); 1459 final float y = event.getY(); 1460 // If the touch event's coordinates are not within the view's header, then delegate 1461 // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume 1462 // and ignore the touch event. 1463 if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) { 1464 return super.onTouchEvent(event); 1465 } else { 1466 return true; 1467 } 1468 } 1469 pointIsInView(float localX, float localY)1470 private final boolean pointIsInView(float localX, float localY) { 1471 return localX >= mLeftOffset && localX < mRightOffset 1472 && localY >= 0 && localY < (getBottom() - getTop()); 1473 } 1474 } 1475