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