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