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