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 593 // Make sure the height is at least as high as the photo 594 height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop()); 595 596 // Make sure height is at least the preferred height 597 height = Math.max(height, preferredHeight); 598 599 // Measure the header if it is visible. 600 if (mHeaderView != null && mHeaderView.getVisibility() == VISIBLE) { 601 mHeaderView.measure( 602 MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY), 603 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 604 } 605 606 setMeasuredDimension(specWidth, height); 607 } 608 609 @Override onLayout(boolean changed, int left, int top, int right, int bottom)610 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 611 final int height = bottom - top; 612 final int width = right - left; 613 614 // Determine the vertical bounds by laying out the header first. 615 int topBound = 0; 616 int bottomBound = height; 617 int leftBound = getPaddingLeft(); 618 int rightBound = width - getPaddingRight(); 619 620 final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this); 621 622 // Put the section header on the left side of the contact view. 623 if (mIsSectionHeaderEnabled) { 624 if (mHeaderView != null) { 625 int headerHeight = mHeaderView.getMeasuredHeight(); 626 int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop; 627 628 mHeaderView.layout( 629 isLayoutRtl ? rightBound - mHeaderWidth : leftBound, 630 headerTopBound, 631 isLayoutRtl ? rightBound : leftBound + mHeaderWidth, 632 headerTopBound + headerHeight); 633 } 634 if (isLayoutRtl) { 635 rightBound -= mHeaderWidth; 636 } else { 637 leftBound += mHeaderWidth; 638 } 639 } 640 641 mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound); 642 mLeftOffset = left + leftBound; 643 mRightOffset = left + rightBound; 644 if (isLayoutRtl) { 645 rightBound -= mGapBetweenIndexerAndImage; 646 } else { 647 leftBound += mGapBetweenIndexerAndImage; 648 } 649 650 if (mActivatedStateSupported && isActivated()) { 651 mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); 652 } 653 654 if (isVisible(mCheckBox)) { 655 final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2; 656 if (mPhotoPosition == PhotoPosition.LEFT) { 657 mCheckBox.layout(rightBound - mGapFromScrollBar - mCheckBoxWidth, 658 photoTop, 659 rightBound - mGapFromScrollBar, 660 photoTop + mCheckBoxHeight); 661 } else { 662 mCheckBox.layout(leftBound + mGapFromScrollBar, 663 photoTop, 664 leftBound + mGapFromScrollBar + mCheckBoxWidth, 665 photoTop + mCheckBoxHeight); 666 } 667 } 668 669 if (isVisible(mDeleteImageButton)) { 670 final int photoTop = topBound + (bottomBound - topBound - mDeleteImageButtonHeight) / 2; 671 final int mDeleteImageButtonSize = mDeleteImageButtonHeight > mDeleteImageButtonWidth 672 ? mDeleteImageButtonHeight : mDeleteImageButtonWidth; 673 if (mPhotoPosition == PhotoPosition.LEFT) { 674 mDeleteImageButton.layout(rightBound - mDeleteImageButtonSize, 675 photoTop, 676 rightBound, 677 photoTop + mDeleteImageButtonSize); 678 rightBound -= mDeleteImageButtonSize; 679 } else { 680 mDeleteImageButton.layout(leftBound, 681 photoTop, 682 leftBound + mDeleteImageButtonSize, 683 photoTop + mDeleteImageButtonSize); 684 leftBound += mDeleteImageButtonSize; 685 } 686 } 687 688 final View photoView = mQuickContact != null ? mQuickContact : mPhotoView; 689 if (mPhotoPosition == PhotoPosition.LEFT) { 690 // Photo is the left most view. All the other Views should on the right of the photo. 691 if (photoView != null) { 692 // Center the photo vertically 693 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2 694 + mAvatarOffsetTop; 695 photoView.layout( 696 leftBound, 697 photoTop, 698 leftBound + mPhotoViewWidth, 699 photoTop + mPhotoViewHeight); 700 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 701 } else if (mKeepHorizontalPaddingForPhotoView) { 702 // Draw nothing but keep the padding. 703 leftBound += mPhotoViewWidth + mGapBetweenImageAndText; 704 } 705 } else { 706 // Photo is the right most view. Right bound should be adjusted that way. 707 if (photoView != null) { 708 // Center the photo vertically 709 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2 710 + mAvatarOffsetTop; 711 photoView.layout( 712 rightBound - mPhotoViewWidth, 713 photoTop, 714 rightBound, 715 photoTop + mPhotoViewHeight); 716 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 717 } else if (mKeepHorizontalPaddingForPhotoView) { 718 // Draw nothing but keep the padding. 719 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); 720 } 721 722 // Add indent between left-most padding and texts. 723 leftBound += mTextIndent; 724 } 725 726 if (mSupportVideoCallIcon) { 727 // Place the video call button at the end of the list (e.g. take into account RTL mode). 728 if (isVisible(mVideoCallIcon)) { 729 // Center the video icon vertically 730 final int videoIconTop = topBound + 731 (bottomBound - topBound - mVideoCallIconSize) / 2; 732 733 if (!isLayoutRtl) { 734 // When photo is on left, video icon is placed on the right edge. 735 mVideoCallIcon.layout(rightBound - mVideoCallIconSize, 736 videoIconTop, 737 rightBound, 738 videoIconTop + mVideoCallIconSize); 739 } else { 740 // When photo is on right, video icon is placed on the left edge. 741 mVideoCallIcon.layout(leftBound, 742 videoIconTop, 743 leftBound + mVideoCallIconSize, 744 videoIconTop + mVideoCallIconSize); 745 } 746 } 747 748 if (mPhotoPosition == PhotoPosition.LEFT) { 749 rightBound -= (mVideoCallIconSize + mVideoCallIconMargin); 750 } else { 751 leftBound += mVideoCallIconSize + mVideoCallIconMargin; 752 } 753 } 754 755 756 // Center text vertically, then apply the top offset. 757 final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight + 758 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight; 759 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop; 760 761 // Work Profile icon align top 762 int workProfileIconWidth = 0; 763 if (isVisible(mWorkProfileIcon)) { 764 workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth(); 765 final int distanceFromEnd = mCheckBoxWidth > 0 766 ? mCheckBoxWidth + mGapBetweenImageAndText : 0; 767 if (mPhotoPosition == PhotoPosition.LEFT) { 768 // When photo is on left, label is placed on the right edge of the list item. 769 mWorkProfileIcon.layout(rightBound - workProfileIconWidth - distanceFromEnd, 770 textTopBound, 771 rightBound - distanceFromEnd, 772 textTopBound + mNameTextViewHeight); 773 } else { 774 // When photo is on right, label is placed on the left of data view. 775 mWorkProfileIcon.layout(leftBound + distanceFromEnd, 776 textTopBound, 777 leftBound + workProfileIconWidth + distanceFromEnd, 778 textTopBound + mNameTextViewHeight); 779 } 780 } 781 782 // Layout all text view and presence icon 783 // Put name TextView first 784 if (isVisible(mNameTextView)) { 785 final int distanceFromEnd = workProfileIconWidth 786 + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0); 787 if (mPhotoPosition == PhotoPosition.LEFT) { 788 mNameTextView.layout(leftBound, 789 textTopBound, 790 rightBound - distanceFromEnd, 791 textTopBound + mNameTextViewHeight); 792 } else { 793 mNameTextView.layout(leftBound + distanceFromEnd, 794 textTopBound, 795 rightBound, 796 textTopBound + mNameTextViewHeight); 797 } 798 } 799 800 if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) { 801 textTopBound += mNameTextViewHeight; 802 } 803 804 // Presence and status 805 if (isLayoutRtl) { 806 int statusRightBound = rightBound; 807 if (isVisible(mPresenceIcon)) { 808 int iconWidth = mPresenceIcon.getMeasuredWidth(); 809 mPresenceIcon.layout( 810 rightBound - iconWidth, 811 textTopBound, 812 rightBound, 813 textTopBound + mStatusTextViewHeight); 814 statusRightBound -= (iconWidth + mPresenceIconMargin); 815 } 816 817 if (isVisible(mStatusView)) { 818 mStatusView.layout(leftBound, 819 textTopBound, 820 statusRightBound, 821 textTopBound + mStatusTextViewHeight); 822 } 823 } else { 824 int statusLeftBound = leftBound; 825 if (isVisible(mPresenceIcon)) { 826 int iconWidth = mPresenceIcon.getMeasuredWidth(); 827 mPresenceIcon.layout( 828 leftBound, 829 textTopBound, 830 leftBound + iconWidth, 831 textTopBound + mStatusTextViewHeight); 832 statusLeftBound += (iconWidth + mPresenceIconMargin); 833 } 834 835 if (isVisible(mStatusView)) { 836 mStatusView.layout(statusLeftBound, 837 textTopBound, 838 rightBound, 839 textTopBound + mStatusTextViewHeight); 840 } 841 } 842 843 if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { 844 textTopBound += mStatusTextViewHeight; 845 } 846 847 // Rest of text views 848 int dataLeftBound = leftBound; 849 if (isVisible(mPhoneticNameTextView)) { 850 mPhoneticNameTextView.layout(leftBound, 851 textTopBound, 852 rightBound, 853 textTopBound + mPhoneticNameTextViewHeight); 854 textTopBound += mPhoneticNameTextViewHeight; 855 } 856 857 // Label and Data align bottom. 858 if (isVisible(mLabelView)) { 859 if (!isLayoutRtl) { 860 mLabelView.layout(dataLeftBound, 861 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, 862 rightBound, 863 textTopBound + mLabelAndDataViewMaxHeight); 864 dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData; 865 } else { 866 dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); 867 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(), 868 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, 869 rightBound, 870 textTopBound + mLabelAndDataViewMaxHeight); 871 rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData); 872 } 873 } 874 875 if (isVisible(mDataView)) { 876 if (!isLayoutRtl) { 877 mDataView.layout(dataLeftBound, 878 textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, 879 rightBound, 880 textTopBound + mLabelAndDataViewMaxHeight); 881 } else { 882 mDataView.layout(rightBound - mDataView.getMeasuredWidth(), 883 textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, 884 rightBound, 885 textTopBound + mLabelAndDataViewMaxHeight); 886 } 887 } 888 if (isVisible(mLabelView) || isVisible(mDataView)) { 889 textTopBound += mLabelAndDataViewMaxHeight; 890 } 891 892 if (isVisible(mSnippetView)) { 893 mSnippetView.layout(leftBound, 894 textTopBound, 895 rightBound, 896 textTopBound + mSnippetTextViewHeight); 897 } 898 } 899 900 @Override adjustListItemSelectionBounds(Rect bounds)901 public void adjustListItemSelectionBounds(Rect bounds) { 902 if (mAdjustSelectionBoundsEnabled) { 903 bounds.top += mBoundsWithoutHeader.top; 904 bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); 905 bounds.left = mBoundsWithoutHeader.left; 906 bounds.right = mBoundsWithoutHeader.right; 907 } 908 } 909 isVisible(View view)910 protected boolean isVisible(View view) { 911 return view != null && view.getVisibility() == View.VISIBLE; 912 } 913 914 /** 915 * Extracts width and height from the style 916 */ ensurePhotoViewSize()917 private void ensurePhotoViewSize() { 918 if (!mPhotoViewWidthAndHeightAreReady) { 919 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); 920 if (!mQuickContactEnabled && mPhotoView == null) { 921 if (!mKeepHorizontalPaddingForPhotoView) { 922 mPhotoViewWidth = 0; 923 } 924 if (!mKeepVerticalPaddingForPhotoView) { 925 mPhotoViewHeight = 0; 926 } 927 } 928 929 mPhotoViewWidthAndHeightAreReady = true; 930 } 931 } 932 getDefaultPhotoViewSize()933 protected int getDefaultPhotoViewSize() { 934 return mDefaultPhotoViewSize; 935 } 936 937 /** 938 * Gets a LayoutParam that corresponds to the default photo size. 939 * 940 * @return A new LayoutParam. 941 */ getDefaultPhotoLayoutParams()942 private LayoutParams getDefaultPhotoLayoutParams() { 943 LayoutParams params = generateDefaultLayoutParams(); 944 params.width = getDefaultPhotoViewSize(); 945 params.height = params.width; 946 return params; 947 } 948 949 @Override drawableStateChanged()950 protected void drawableStateChanged() { 951 super.drawableStateChanged(); 952 if (mActivatedStateSupported) { 953 mActivatedBackgroundDrawable.setState(getDrawableState()); 954 } 955 } 956 957 @Override verifyDrawable(Drawable who)958 protected boolean verifyDrawable(Drawable who) { 959 return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); 960 } 961 962 @Override jumpDrawablesToCurrentState()963 public void jumpDrawablesToCurrentState() { 964 super.jumpDrawablesToCurrentState(); 965 if (mActivatedStateSupported) { 966 mActivatedBackgroundDrawable.jumpToCurrentState(); 967 } 968 } 969 970 @Override dispatchDraw(Canvas canvas)971 public void dispatchDraw(Canvas canvas) { 972 if (mActivatedStateSupported && isActivated()) { 973 mActivatedBackgroundDrawable.draw(canvas); 974 } 975 976 super.dispatchDraw(canvas); 977 } 978 979 /** 980 * Sets section header or makes it invisible if the title is null. 981 */ setSectionHeader(String title)982 public void setSectionHeader(String title) { 983 if (title != null) { 984 // Empty section title is the favorites so show the star here. 985 if (title.isEmpty()) { 986 if (mHeaderView == null) { 987 addStarImageHeader(); 988 } else if (mHeaderView instanceof TextView) { 989 removeView(mHeaderView); 990 addStarImageHeader(); 991 } else { 992 mHeaderView.setVisibility(View.VISIBLE); 993 } 994 } else { 995 if (mHeaderView == null) { 996 addTextHeader(title); 997 } else if (mHeaderView instanceof ImageView) { 998 removeView(mHeaderView); 999 addTextHeader(title); 1000 } else { 1001 updateHeaderText((TextView) mHeaderView, title); 1002 } 1003 } 1004 } else if (mHeaderView != null) { 1005 mHeaderView.setVisibility(View.GONE); 1006 } 1007 } 1008 addTextHeader(String title)1009 private void addTextHeader(String title) { 1010 mHeaderView = new TextView(getContext()); 1011 final TextView headerTextView = (TextView) mHeaderView; 1012 headerTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle); 1013 headerTextView.setGravity(Gravity.CENTER_HORIZONTAL); 1014 updateHeaderText(headerTextView, title); 1015 addView(headerTextView); 1016 } 1017 updateHeaderText(TextView headerTextView, String title)1018 private void updateHeaderText(TextView headerTextView, String title) { 1019 setMarqueeText(headerTextView, title); 1020 headerTextView.setAllCaps(true); 1021 if (ContactsSectionIndexer.BLANK_HEADER_STRING.equals(title)) { 1022 headerTextView.setContentDescription( 1023 getContext().getString(R.string.description_no_name_header)); 1024 } else { 1025 headerTextView.setContentDescription(title); 1026 } 1027 headerTextView.setVisibility(View.VISIBLE); 1028 } 1029 addStarImageHeader()1030 private void addStarImageHeader() { 1031 mHeaderView = new ImageView(getContext()); 1032 final ImageView headerImageView = (ImageView) mHeaderView; 1033 headerImageView.setImageDrawable( 1034 getResources().getDrawable(R.drawable.quantum_ic_star_vd_theme_24, 1035 getContext().getTheme())); 1036 headerImageView.setImageTintList(ColorStateList.valueOf(getResources() 1037 .getColor(R.color.material_star_pink))); 1038 headerImageView.setContentDescription( 1039 getContext().getString(R.string.contactsFavoritesLabel)); 1040 headerImageView.setVisibility(View.VISIBLE); 1041 addView(headerImageView); 1042 } 1043 setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled)1044 public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) { 1045 mIsSectionHeaderEnabled = isSectionHeaderEnabled; 1046 } 1047 1048 /** 1049 * Returns the quick contact badge, creating it if necessary. 1050 */ getQuickContact()1051 public QuickContactBadge getQuickContact() { 1052 if (!mQuickContactEnabled) { 1053 throw new IllegalStateException("QuickContact is disabled for this view"); 1054 } 1055 if (mQuickContact == null) { 1056 mQuickContact = new QuickContactBadge(getContext()); 1057 if (CompatUtils.isLollipopCompatible()) { 1058 mQuickContact.setOverlay(null); 1059 } 1060 mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams()); 1061 if (mNameTextView != null) { 1062 mQuickContact.setContentDescription(getContext().getString( 1063 R.string.description_quick_contact_for, mNameTextView.getText())); 1064 } 1065 1066 addView(mQuickContact); 1067 mPhotoViewWidthAndHeightAreReady = false; 1068 } 1069 return mQuickContact; 1070 } 1071 1072 /** 1073 * Returns the photo view, creating it if necessary. 1074 */ getPhotoView()1075 public ImageView getPhotoView() { 1076 if (mPhotoView == null) { 1077 mPhotoView = new ImageView(getContext()); 1078 mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams()); 1079 // Quick contact style used above will set a background - remove it 1080 mPhotoView.setBackground(null); 1081 addView(mPhotoView); 1082 mPhotoViewWidthAndHeightAreReady = false; 1083 } 1084 return mPhotoView; 1085 } 1086 1087 /** 1088 * Removes the photo view. 1089 */ removePhotoView()1090 public void removePhotoView() { 1091 removePhotoView(false, true); 1092 } 1093 1094 /** 1095 * Removes the photo view. 1096 * 1097 * @param keepHorizontalPadding True means data on the right side will have 1098 * padding on left, pretending there is still a photo view. 1099 * @param keepVerticalPadding True means the View will have some height 1100 * enough for accommodating a photo view. 1101 */ removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)1102 public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { 1103 mPhotoViewWidthAndHeightAreReady = false; 1104 mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; 1105 mKeepVerticalPaddingForPhotoView = keepVerticalPadding; 1106 if (mPhotoView != null) { 1107 removeView(mPhotoView); 1108 mPhotoView = null; 1109 } 1110 if (mQuickContact != null) { 1111 removeView(mQuickContact); 1112 mQuickContact = null; 1113 } 1114 } 1115 1116 /** 1117 * Sets a word prefix that will be highlighted if encountered in fields like 1118 * name and search snippet. This will disable the mask highlighting for names. 1119 * <p> 1120 * NOTE: must be all upper-case 1121 */ setHighlightedPrefix(String upperCasePrefix)1122 public void setHighlightedPrefix(String upperCasePrefix) { 1123 mHighlightedPrefix = upperCasePrefix; 1124 } 1125 1126 /** 1127 * Clears previously set highlight sequences for the view. 1128 */ clearHighlightSequences()1129 public void clearHighlightSequences() { 1130 mNameHighlightSequence.clear(); 1131 mNumberHighlightSequence.clear(); 1132 mHighlightedPrefix = null; 1133 } 1134 1135 /** 1136 * Adds a highlight sequence to the name highlighter. 1137 * @param start The start position of the highlight sequence. 1138 * @param end The end position of the highlight sequence. 1139 */ addNameHighlightSequence(int start, int end)1140 public void addNameHighlightSequence(int start, int end) { 1141 mNameHighlightSequence.add(new HighlightSequence(start, end)); 1142 } 1143 1144 /** 1145 * Adds a highlight sequence to the number highlighter. 1146 * @param start The start position of the highlight sequence. 1147 * @param end The end position of the highlight sequence. 1148 */ addNumberHighlightSequence(int start, int end)1149 public void addNumberHighlightSequence(int start, int end) { 1150 mNumberHighlightSequence.add(new HighlightSequence(start, end)); 1151 } 1152 1153 /** 1154 * Returns the text view for the contact name, creating it if necessary. 1155 */ getNameTextView()1156 public TextView getNameTextView() { 1157 if (mNameTextView == null) { 1158 mNameTextView = new TextView(getContext()); 1159 mNameTextView.setSingleLine(true); 1160 mNameTextView.setEllipsize(getTextEllipsis()); 1161 mNameTextView.setTextColor(ResourcesCompat.getColorStateList(getResources(), 1162 R.color.contact_list_name_text_color, getContext().getTheme())); 1163 mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize); 1164 // Manually call setActivated() since this view may be added after the first 1165 // setActivated() call toward this whole item view. 1166 mNameTextView.setActivated(isActivated()); 1167 mNameTextView.setGravity(Gravity.CENTER_VERTICAL); 1168 mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1169 mNameTextView.setId(R.id.cliv_name_textview); 1170 if (CompatUtils.isLollipopCompatible()) { 1171 mNameTextView.setElegantTextHeight(false); 1172 } 1173 addView(mNameTextView); 1174 } 1175 return mNameTextView; 1176 } 1177 1178 /** 1179 * Adds or updates a text view for the phonetic name. 1180 */ setPhoneticName(char[] text, int size)1181 public void setPhoneticName(char[] text, int size) { 1182 if (text == null || size == 0) { 1183 if (mPhoneticNameTextView != null) { 1184 mPhoneticNameTextView.setVisibility(View.GONE); 1185 } 1186 } else { 1187 getPhoneticNameTextView(); 1188 setMarqueeText(mPhoneticNameTextView, text, size); 1189 mPhoneticNameTextView.setVisibility(VISIBLE); 1190 } 1191 } 1192 1193 /** 1194 * Returns the text view for the phonetic name, creating it if necessary. 1195 */ getPhoneticNameTextView()1196 public TextView getPhoneticNameTextView() { 1197 if (mPhoneticNameTextView == null) { 1198 mPhoneticNameTextView = new TextView(getContext()); 1199 mPhoneticNameTextView.setSingleLine(true); 1200 mPhoneticNameTextView.setEllipsize(getTextEllipsis()); 1201 mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 1202 mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1203 mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); 1204 mPhoneticNameTextView.setActivated(isActivated()); 1205 mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview); 1206 addView(mPhoneticNameTextView); 1207 } 1208 return mPhoneticNameTextView; 1209 } 1210 1211 /** 1212 * Adds or updates a text view for the data label. 1213 */ setLabel(CharSequence text)1214 public void setLabel(CharSequence text) { 1215 if (TextUtils.isEmpty(text)) { 1216 if (mLabelView != null) { 1217 mLabelView.setVisibility(View.GONE); 1218 } 1219 } else { 1220 getLabelView(); 1221 setMarqueeText(mLabelView, text); 1222 mLabelView.setVisibility(VISIBLE); 1223 } 1224 } 1225 1226 /** 1227 * Returns the text view for the data label, creating it if necessary. 1228 */ getLabelView()1229 public TextView getLabelView() { 1230 if (mLabelView == null) { 1231 mLabelView = new TextView(getContext()); 1232 mLabelView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, 1233 LayoutParams.WRAP_CONTENT)); 1234 1235 mLabelView.setSingleLine(true); 1236 mLabelView.setEllipsize(getTextEllipsis()); 1237 mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); 1238 if (mPhotoPosition == PhotoPosition.LEFT) { 1239 mLabelView.setAllCaps(true); 1240 } else { 1241 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); 1242 } 1243 mLabelView.setActivated(isActivated()); 1244 mLabelView.setId(R.id.cliv_label_textview); 1245 addView(mLabelView); 1246 } 1247 return mLabelView; 1248 } 1249 1250 /** 1251 * Adds or updates a text view for the data element. 1252 */ setData(char[] text, int size)1253 public void setData(char[] text, int size) { 1254 if (text == null || size == 0) { 1255 if (mDataView != null) { 1256 mDataView.setVisibility(View.GONE); 1257 } 1258 } else { 1259 getDataView(); 1260 setMarqueeText(mDataView, text, size); 1261 mDataView.setVisibility(VISIBLE); 1262 } 1263 } 1264 1265 /** 1266 * Sets phone number for a list item. This takes care of number highlighting if the highlight 1267 * mask exists. 1268 */ setPhoneNumber(String text, String countryIso)1269 public void setPhoneNumber(String text, String countryIso) { 1270 if (text == null) { 1271 if (mDataView != null) { 1272 mDataView.setVisibility(View.GONE); 1273 } 1274 } else { 1275 getDataView(); 1276 1277 // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to 1278 // mDataView. Make sure that determination of the highlight sequences are done only 1279 // after number formatting. 1280 1281 // Sets phone number texts for display after highlighting it, if applicable. 1282 // CharSequence textToSet = text; 1283 final SpannableString textToSet = new SpannableString(text); 1284 1285 if (mNumberHighlightSequence.size() != 0) { 1286 final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0); 1287 mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start, 1288 highlightSequence.end); 1289 } 1290 1291 setMarqueeText(mDataView, textToSet); 1292 mDataView.setVisibility(VISIBLE); 1293 1294 // We have a phone number as "mDataView" so make it always LTR and VIEW_START 1295 mDataView.setTextDirection(View.TEXT_DIRECTION_LTR); 1296 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1297 } 1298 } 1299 setMarqueeText(TextView textView, char[] text, int size)1300 private void setMarqueeText(TextView textView, char[] text, int size) { 1301 if (getTextEllipsis() == TruncateAt.MARQUEE) { 1302 setMarqueeText(textView, new String(text, 0, size)); 1303 } else { 1304 textView.setText(text, 0, size); 1305 } 1306 } 1307 setMarqueeText(TextView textView, CharSequence text)1308 private void setMarqueeText(TextView textView, CharSequence text) { 1309 if (getTextEllipsis() == TruncateAt.MARQUEE) { 1310 // To show MARQUEE correctly (with END effect during non-active state), we need 1311 // to build Spanned with MARQUEE in addition to TextView's ellipsize setting. 1312 final SpannableString spannable = new SpannableString(text); 1313 spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(), 1314 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1315 textView.setText(spannable); 1316 } else { 1317 textView.setText(text); 1318 } 1319 } 1320 1321 /** 1322 * Returns the {@link AppCompatCheckBox} view, creating it if necessary. 1323 */ getCheckBox()1324 public AppCompatCheckBox getCheckBox() { 1325 if (mCheckBox == null) { 1326 mCheckBox = new AppCompatCheckBox(getContext()); 1327 // Make non-focusable, so the rest of the ContactListItemView can be clicked. 1328 mCheckBox.setFocusable(false); 1329 addView(mCheckBox); 1330 } 1331 return mCheckBox; 1332 } 1333 1334 /** 1335 * Returns the {@link AppCompatImageButton} delete button, creating it if necessary. 1336 */ getDeleteImageButton( final MultiSelectEntryContactListAdapter.DeleteContactListener listener, final int position)1337 public AppCompatImageButton getDeleteImageButton( 1338 final MultiSelectEntryContactListAdapter.DeleteContactListener listener, 1339 final int position) { 1340 if (mDeleteImageButton == null) { 1341 mDeleteImageButton = new AppCompatImageButton(getContext()); 1342 mDeleteImageButton.setImageResource(R.drawable.quantum_ic_cancel_vd_theme_24); 1343 mDeleteImageButton.setScaleType(ScaleType.CENTER); 1344 mDeleteImageButton.setBackgroundColor(Color.TRANSPARENT); 1345 mDeleteImageButton.setContentDescription( 1346 getResources().getString(R.string.description_delete_contact)); 1347 if (CompatUtils. isLollipopCompatible()) { 1348 final TypedValue typedValue = new TypedValue(); 1349 getContext().getTheme().resolveAttribute( 1350 android.R.attr.selectableItemBackgroundBorderless, typedValue, true); 1351 mDeleteImageButton.setBackgroundResource(typedValue.resourceId); 1352 } 1353 addView(mDeleteImageButton); 1354 } 1355 // Reset onClickListener because after reloading the view, position might be changed. 1356 mDeleteImageButton.setOnClickListener(new OnClickListener() { 1357 @Override 1358 public void onClick(View v) { 1359 // Inform the adapter that delete icon was clicked. 1360 if (listener != null) { 1361 listener.onContactDeleteClicked(position); 1362 } 1363 } 1364 }); 1365 return mDeleteImageButton; 1366 } 1367 1368 /** 1369 * Returns the text view for the data text, creating it if necessary. 1370 */ getDataView()1371 public TextView getDataView() { 1372 if (mDataView == null) { 1373 mDataView = new TextView(getContext()); 1374 mDataView.setSingleLine(true); 1375 mDataView.setEllipsize(getTextEllipsis()); 1376 mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); 1377 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1378 mDataView.setActivated(isActivated()); 1379 mDataView.setId(R.id.cliv_data_view); 1380 if (CompatUtils.isLollipopCompatible()) { 1381 mDataView.setElegantTextHeight(false); 1382 } 1383 addView(mDataView); 1384 } 1385 return mDataView; 1386 } 1387 1388 /** 1389 * Adds or updates a text view for the search snippet. 1390 */ setSnippet(String text)1391 public void setSnippet(String text) { 1392 if (TextUtils.isEmpty(text)) { 1393 if (mSnippetView != null) { 1394 mSnippetView.setVisibility(View.GONE); 1395 } 1396 } else { 1397 mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); 1398 mSnippetView.setVisibility(VISIBLE); 1399 if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { 1400 // Give the text-to-speech engine a hint that it's a phone number 1401 mSnippetView.setContentDescription( 1402 PhoneNumberUtilsCompat.createTtsSpannable(text)); 1403 } else { 1404 mSnippetView.setContentDescription(null); 1405 } 1406 } 1407 } 1408 1409 /** 1410 * Returns the text view for the search snippet, creating it if necessary. 1411 */ getSnippetView()1412 public TextView getSnippetView() { 1413 if (mSnippetView == null) { 1414 mSnippetView = new TextView(getContext()); 1415 mSnippetView.setSingleLine(true); 1416 mSnippetView.setEllipsize(getTextEllipsis()); 1417 mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 1418 mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1419 mSnippetView.setActivated(isActivated()); 1420 addView(mSnippetView); 1421 } 1422 return mSnippetView; 1423 } 1424 1425 /** 1426 * Returns the text view for the status, creating it if necessary. 1427 */ getStatusView()1428 public TextView getStatusView() { 1429 if (mStatusView == null) { 1430 mStatusView = new TextView(getContext()); 1431 mStatusView.setSingleLine(true); 1432 mStatusView.setEllipsize(getTextEllipsis()); 1433 mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); 1434 mStatusView.setTextColor(mSecondaryTextColor); 1435 mStatusView.setActivated(isActivated()); 1436 mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); 1437 addView(mStatusView); 1438 } 1439 return mStatusView; 1440 } 1441 1442 /** 1443 * Adds or updates a text view for the status. 1444 */ setStatus(CharSequence text)1445 public void setStatus(CharSequence text) { 1446 if (TextUtils.isEmpty(text)) { 1447 if (mStatusView != null) { 1448 mStatusView.setVisibility(View.GONE); 1449 } 1450 } else { 1451 getStatusView(); 1452 setMarqueeText(mStatusView, text); 1453 mStatusView.setVisibility(VISIBLE); 1454 } 1455 } 1456 1457 /** 1458 * Adds or updates the presence icon view. 1459 */ setPresence(Drawable icon)1460 public void setPresence(Drawable icon) { 1461 if (icon != null) { 1462 if (mPresenceIcon == null) { 1463 mPresenceIcon = new ImageView(getContext()); 1464 addView(mPresenceIcon); 1465 } 1466 mPresenceIcon.setImageDrawable(icon); 1467 mPresenceIcon.setScaleType(ScaleType.CENTER); 1468 mPresenceIcon.setVisibility(View.VISIBLE); 1469 } else { 1470 if (mPresenceIcon != null) { 1471 mPresenceIcon.setVisibility(View.GONE); 1472 } 1473 } 1474 } 1475 1476 /** 1477 * Set to display work profile icon or not 1478 * 1479 * @param enabled set to display work profile icon or not 1480 */ setWorkProfileIconEnabled(boolean enabled)1481 public void setWorkProfileIconEnabled(boolean enabled) { 1482 if (mWorkProfileIcon != null) { 1483 mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE); 1484 } else if (enabled) { 1485 mWorkProfileIcon = new ImageView(getContext()); 1486 addView(mWorkProfileIcon); 1487 mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile); 1488 mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE); 1489 mWorkProfileIcon.setVisibility(View.VISIBLE); 1490 } 1491 } 1492 getTextEllipsis()1493 private TruncateAt getTextEllipsis() { 1494 return TruncateAt.MARQUEE; 1495 } 1496 showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)1497 public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) { 1498 CharSequence name = cursor.getString(nameColumnIndex); 1499 setDisplayName(name); 1500 1501 // Since the quick contact content description is derived from the display name and there is 1502 // no guarantee that when the quick contact is initialized the display name is already set, 1503 // do it here too. 1504 if (mQuickContact != null) { 1505 mQuickContact.setContentDescription(getContext().getString( 1506 R.string.description_quick_contact_for, mNameTextView.getText())); 1507 } 1508 } 1509 setDisplayName(CharSequence name, boolean highlight)1510 public void setDisplayName(CharSequence name, boolean highlight) { 1511 if (!TextUtils.isEmpty(name) && highlight) { 1512 clearHighlightSequences(); 1513 addNameHighlightSequence(0, name.length()); 1514 } 1515 setDisplayName(name); 1516 } 1517 setDisplayName(CharSequence name)1518 public void setDisplayName(CharSequence name) { 1519 if (!TextUtils.isEmpty(name)) { 1520 // Chooses the available highlighting method for highlighting. 1521 if (mHighlightedPrefix != null) { 1522 name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix); 1523 } else if (mNameHighlightSequence.size() != 0) { 1524 final SpannableString spannableName = new SpannableString(name); 1525 for (HighlightSequence highlightSequence : mNameHighlightSequence) { 1526 mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start, 1527 highlightSequence.end); 1528 } 1529 name = spannableName; 1530 } 1531 } else { 1532 name = mUnknownNameText; 1533 } 1534 setMarqueeText(getNameTextView(), name); 1535 1536 if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { 1537 // Give the text-to-speech engine a hint that it's a phone number 1538 mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR); 1539 mNameTextView.setContentDescription( 1540 PhoneNumberUtilsCompat.createTtsSpannable(name.toString())); 1541 } else { 1542 // Remove span tags of highlighting for talkback to avoid reading highlighting and rest 1543 // of the name into two separate parts. 1544 mNameTextView.setContentDescription(name.toString()); 1545 } 1546 } 1547 hideCheckBox()1548 public void hideCheckBox() { 1549 if (mCheckBox != null) { 1550 removeView(mCheckBox); 1551 mCheckBox = null; 1552 } 1553 } 1554 hideDeleteImageButton()1555 public void hideDeleteImageButton() { 1556 if (mDeleteImageButton != null) { 1557 removeView(mDeleteImageButton); 1558 mDeleteImageButton = null; 1559 } 1560 } 1561 hideDisplayName()1562 public void hideDisplayName() { 1563 if (mNameTextView != null) { 1564 removeView(mNameTextView); 1565 mNameTextView = null; 1566 } 1567 } 1568 showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)1569 public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) { 1570 cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer); 1571 int phoneticNameSize = mPhoneticNameBuffer.sizeCopied; 1572 if (phoneticNameSize != 0) { 1573 setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize); 1574 } else { 1575 setPhoneticName(null, 0); 1576 } 1577 } 1578 hidePhoneticName()1579 public void hidePhoneticName() { 1580 if (mPhoneticNameTextView != null) { 1581 removeView(mPhoneticNameTextView); 1582 mPhoneticNameTextView = null; 1583 } 1584 } 1585 1586 /** 1587 * Sets the proper icon (star or presence or nothing) and/or status message. 1588 */ showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)1589 public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, 1590 int contactStatusColumnIndex) { 1591 Drawable icon = null; 1592 int presence = 0; 1593 if (!cursor.isNull(presenceColumnIndex)) { 1594 presence = cursor.getInt(presenceColumnIndex); 1595 icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); 1596 } 1597 setPresence(icon); 1598 1599 String statusMessage = null; 1600 if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { 1601 statusMessage = cursor.getString(contactStatusColumnIndex); 1602 } 1603 // If there is no status message from the contact, but there was a presence value, then use 1604 // the default status message string 1605 if (statusMessage == null && presence != 0) { 1606 statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); 1607 } 1608 setStatus(statusMessage); 1609 } 1610 1611 /** 1612 * Shows search snippet for email and phone number matches. 1613 */ showSnippet(Cursor cursor, String query, int snippetColumn)1614 public void showSnippet(Cursor cursor, String query, int snippetColumn) { 1615 // TODO: this does not properly handle phone numbers with control characters 1616 // For example if the phone number is 444-5555, the search query 4445 will match the 1617 // number since we normalize it before querying CP2 but the snippet will fail since 1618 // the portion to be highlighted is 444-5 not 4445. 1619 final String snippet = cursor.getString(snippetColumn); 1620 if (snippet == null) { 1621 setSnippet(null); 1622 return; 1623 } 1624 final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0 1625 ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null; 1626 if (snippet.equals(displayName)) { 1627 // If the snippet exactly matches the display name (i.e. the phone number or email 1628 // address is being used as the display name) then no snippet is necessary 1629 setSnippet(null); 1630 return; 1631 } 1632 // Show the snippet with the part of the query that matched it 1633 setSnippet(updateSnippet(snippet, query, displayName)); 1634 } 1635 1636 /** 1637 * Shows search snippet. 1638 */ showSnippet(Cursor cursor, int summarySnippetColumnIndex)1639 public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { 1640 if (cursor.getColumnCount() <= summarySnippetColumnIndex 1641 || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) { 1642 setSnippet(null); 1643 return; 1644 } 1645 1646 String snippet = cursor.getString(summarySnippetColumnIndex); 1647 1648 // Do client side snippeting if provider didn't do it 1649 final Bundle extras = cursor.getExtras(); 1650 if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { 1651 1652 final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY); 1653 1654 String displayName = null; 1655 int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); 1656 if (displayNameIndex >= 0) { 1657 displayName = cursor.getString(displayNameIndex); 1658 } 1659 1660 snippet = updateSnippet(snippet, query, displayName); 1661 1662 } else { 1663 if (snippet != null) { 1664 int from = 0; 1665 int to = snippet.length(); 1666 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH); 1667 if (start == -1) { 1668 snippet = null; 1669 } else { 1670 int firstNl = snippet.lastIndexOf('\n', start); 1671 if (firstNl != -1) { 1672 from = firstNl + 1; 1673 } 1674 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH); 1675 if (end != -1) { 1676 int lastNl = snippet.indexOf('\n', end); 1677 if (lastNl != -1) { 1678 to = lastNl; 1679 } 1680 } 1681 1682 StringBuilder sb = new StringBuilder(); 1683 for (int i = from; i < to; i++) { 1684 char c = snippet.charAt(i); 1685 if (c != DefaultContactListAdapter.SNIPPET_START_MATCH && 1686 c != DefaultContactListAdapter.SNIPPET_END_MATCH) { 1687 sb.append(c); 1688 } 1689 } 1690 snippet = sb.toString(); 1691 } 1692 } 1693 } 1694 1695 setSnippet(snippet); 1696 } 1697 1698 /** 1699 * Used for deferred snippets from the database. The contents come back as large strings which 1700 * need to be extracted for display. 1701 * 1702 * @param snippet The snippet from the database. 1703 * @param query The search query substring. 1704 * @param displayName The contact display name. 1705 * @return The proper snippet to display. 1706 */ updateSnippet(String snippet, String query, String displayName)1707 private String updateSnippet(String snippet, String query, String displayName) { 1708 1709 if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) { 1710 return null; 1711 } 1712 query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase()); 1713 1714 // If the display name already contains the query term, return empty - snippets should 1715 // not be needed in that case. 1716 if (!TextUtils.isEmpty(displayName)) { 1717 final String lowerDisplayName = displayName.toLowerCase(); 1718 final List<String> nameTokens = split(lowerDisplayName); 1719 for (String nameToken : nameTokens) { 1720 if (nameToken.startsWith(query)) { 1721 return null; 1722 } 1723 } 1724 } 1725 1726 // The snippet may contain multiple data lines. 1727 // Show the first line that matches the query. 1728 final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query); 1729 1730 if (matched != null && matched.line != null) { 1731 // Tokenize for long strings since the match may be at the end of it. 1732 // Skip this part for short strings since the whole string will be displayed. 1733 // Most contact strings are short so the snippetize method will be called infrequently. 1734 final int lengthThreshold = getResources().getInteger( 1735 R.integer.snippet_length_before_tokenize); 1736 if (matched.line.length() > lengthThreshold) { 1737 return snippetize(matched.line, matched.startIndex, lengthThreshold); 1738 } else { 1739 return matched.line; 1740 } 1741 } 1742 1743 // No match found. 1744 return null; 1745 } 1746 snippetize(String line, int matchIndex, int maxLength)1747 private String snippetize(String line, int matchIndex, int maxLength) { 1748 // Show up to maxLength characters. But we only show full tokens so show the last full token 1749 // up to maxLength characters. So as many starting tokens as possible before trying ending 1750 // tokens. 1751 int remainingLength = maxLength; 1752 int tempRemainingLength = remainingLength; 1753 1754 // Start the end token after the matched query. 1755 int index = matchIndex; 1756 int endTokenIndex = index; 1757 1758 // Find the match token first. 1759 while (index < line.length()) { 1760 if (!Character.isLetterOrDigit(line.charAt(index))) { 1761 endTokenIndex = index; 1762 remainingLength = tempRemainingLength; 1763 break; 1764 } 1765 tempRemainingLength--; 1766 index++; 1767 } 1768 1769 // Find as much content before the match. 1770 index = matchIndex - 1; 1771 tempRemainingLength = remainingLength; 1772 int startTokenIndex = matchIndex; 1773 while (index > -1 && tempRemainingLength > 0) { 1774 if (!Character.isLetterOrDigit(line.charAt(index))) { 1775 startTokenIndex = index; 1776 remainingLength = tempRemainingLength; 1777 } 1778 tempRemainingLength--; 1779 index--; 1780 } 1781 1782 index = endTokenIndex; 1783 tempRemainingLength = remainingLength; 1784 // Find remaining content at after match. 1785 while (index < line.length() && tempRemainingLength > 0) { 1786 if (!Character.isLetterOrDigit(line.charAt(index))) { 1787 endTokenIndex = index; 1788 } 1789 tempRemainingLength--; 1790 index++; 1791 } 1792 // Append ellipse if there is content before or after. 1793 final StringBuilder sb = new StringBuilder(); 1794 if (startTokenIndex > 0) { 1795 sb.append("..."); 1796 } 1797 sb.append(line.substring(startTokenIndex, endTokenIndex)); 1798 if (endTokenIndex < line.length()) { 1799 sb.append("..."); 1800 } 1801 return sb.toString(); 1802 } 1803 1804 private static final Pattern SPLIT_PATTERN = Pattern.compile( 1805 "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); 1806 1807 /** 1808 * Helper method for splitting a string into tokens. The lists passed in are populated with 1809 * the 1810 * tokens and offsets into the content of each token. The tokenization function parses e-mail 1811 * addresses as a single token; otherwise it splits on any non-alphanumeric character. 1812 * 1813 * @param content Content to split. 1814 * @return List of token strings. 1815 */ split(String content)1816 private static List<String> split(String content) { 1817 final Matcher matcher = SPLIT_PATTERN.matcher(content); 1818 final ArrayList<String> tokens = Lists.newArrayList(); 1819 while (matcher.find()) { 1820 tokens.add(matcher.group()); 1821 } 1822 return tokens; 1823 } 1824 1825 /** 1826 * Shows data element. 1827 */ showData(Cursor cursor, int dataColumnIndex)1828 public void showData(Cursor cursor, int dataColumnIndex) { 1829 cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); 1830 setData(mDataBuffer.data, mDataBuffer.sizeCopied); 1831 } 1832 setActivatedStateSupported(boolean flag)1833 public void setActivatedStateSupported(boolean flag) { 1834 this.mActivatedStateSupported = flag; 1835 } 1836 setAdjustSelectionBoundsEnabled(boolean enabled)1837 public void setAdjustSelectionBoundsEnabled(boolean enabled) { 1838 mAdjustSelectionBoundsEnabled = enabled; 1839 } 1840 1841 @Override requestLayout()1842 public void requestLayout() { 1843 // We will assume that once measured this will not need to resize 1844 // itself, so there is no need to pass the layout request to the parent 1845 // view (ListView). 1846 forceLayout(); 1847 } 1848 setPhotoPosition(PhotoPosition photoPosition)1849 public void setPhotoPosition(PhotoPosition photoPosition) { 1850 mPhotoPosition = photoPosition; 1851 } 1852 getPhotoPosition()1853 public PhotoPosition getPhotoPosition() { 1854 return mPhotoPosition; 1855 } 1856 1857 /** 1858 * Set drawable resources directly for the drawable resource of the photo view. 1859 * 1860 * @param drawableId Id of drawable resource. 1861 */ setDrawableResource(int drawableId)1862 public void setDrawableResource(int drawableId) { 1863 ImageView photo = getPhotoView(); 1864 photo.setScaleType(ImageView.ScaleType.CENTER); 1865 final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId); 1866 final int iconColor = 1867 ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color); 1868 if (CompatUtils.isLollipopCompatible()) { 1869 photo.setImageDrawable(drawable); 1870 photo.setImageTintList(ColorStateList.valueOf(iconColor)); 1871 } else { 1872 final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate(); 1873 DrawableCompat.setTint(drawableWrapper, iconColor); 1874 photo.setImageDrawable(drawableWrapper); 1875 } 1876 } 1877 1878 @Override onTouchEvent(MotionEvent event)1879 public boolean onTouchEvent(MotionEvent event) { 1880 final float x = event.getX(); 1881 final float y = event.getY(); 1882 // If the touch event's coordinates are not within the view's header, then delegate 1883 // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume 1884 // and ignore the touch event. 1885 if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) { 1886 return super.onTouchEvent(event); 1887 } else { 1888 return true; 1889 } 1890 } 1891 pointIsInView(float localX, float localY)1892 private final boolean pointIsInView(float localX, float localY) { 1893 return localX >= mLeftOffset && localX < mRightOffset 1894 && localY >= 0 && localY < (getBottom() - getTop()); 1895 } 1896 } 1897