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