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