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