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