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