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 com.android.contacts.ContactPresenceIconUtil;
     20 import com.android.contacts.ContactStatusUtil;
     21 import com.android.contacts.R;
     22 import com.android.contacts.format.PrefixHighlighter;
     23 
     24 import android.content.Context;
     25 import android.content.res.ColorStateList;
     26 import android.content.res.TypedArray;
     27 import android.database.CharArrayBuffer;
     28 import android.database.Cursor;
     29 import android.graphics.Canvas;
     30 import android.graphics.Color;
     31 import android.graphics.Rect;
     32 import android.graphics.Typeface;
     33 import android.graphics.drawable.Drawable;
     34 import android.os.Bundle;
     35 import android.provider.ContactsContract;
     36 import android.provider.ContactsContract.Contacts;
     37 import android.text.Spannable;
     38 import android.text.SpannableString;
     39 import android.text.TextUtils;
     40 import android.text.TextUtils.TruncateAt;
     41 import android.util.AttributeSet;
     42 import android.util.TypedValue;
     43 import android.view.Gravity;
     44 import android.view.View;
     45 import android.view.ViewGroup;
     46 import android.widget.AbsListView.SelectionBoundsAdjuster;
     47 import android.widget.ImageView;
     48 import android.widget.ImageView.ScaleType;
     49 import android.widget.QuickContactBadge;
     50 import android.widget.TextView;
     51 
     52 /**
     53  * A custom view for an item in the contact list.
     54  * The view contains the contact's photo, a set of text views (for name, status, etc...) and
     55  * icons for presence and call.
     56  * The view uses no XML file for layout and all the measurements and layouts are done
     57  * in the onMeasure and onLayout methods.
     58  *
     59  * The layout puts the contact's photo on the right side of the view, the call icon (if present)
     60  * to the left of the photo, the text lines are aligned to the left and the presence icon (if
     61  * present) is set to the left of the status line.
     62  *
     63  * The layout also supports a header (used as a header of a group of contacts) that is above the
     64  * contact's data and a divider between contact view.
     65  */
     66 
     67 public class ContactListItemView extends ViewGroup
     68         implements SelectionBoundsAdjuster {
     69 
     70     private static final int QUICK_CONTACT_BADGE_STYLE =
     71             com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
     72 
     73     protected final Context mContext;
     74 
     75     // Style values for layout and appearance
     76     private final int mPreferredHeight;
     77     private final int mVerticalDividerMargin;
     78     private final int mGapBetweenImageAndText;
     79     private final int mGapBetweenLabelAndData;
     80     private final int mCallButtonPadding;
     81     private final int mPresenceIconMargin;
     82     private final int mPresenceIconSize;
     83     private final int mHeaderTextColor;
     84     private final int mHeaderTextIndent;
     85     private final int mHeaderTextSize;
     86     private final int mHeaderUnderlineHeight;
     87     private final int mHeaderUnderlineColor;
     88     private final int mCountViewTextSize;
     89     private final int mContactsCountTextColor;
     90     private final int mTextIndent;
     91     private Drawable mActivatedBackgroundDrawable;
     92 
     93     /**
     94      * Used with {@link #mLabelView}, specifying the width ratio between label and data.
     95      */
     96     private final int mLabelViewWidthWeight;
     97     /**
     98      * Used with {@link #mDataView}, specifying the width ratio between label and data.
     99      */
    100     private final int mDataViewWidthWeight;
    101 
    102     // Will be used with adjustListItemSelectionBounds().
    103     private int mSelectionBoundsMarginLeft;
    104     private int mSelectionBoundsMarginRight;
    105 
    106     // Horizontal divider between contact views.
    107     private boolean mHorizontalDividerVisible = true;
    108     private Drawable mHorizontalDividerDrawable;
    109     private int mHorizontalDividerHeight;
    110 
    111     /**
    112      * Where to put contact photo. This affects the other Views' layout or look-and-feel.
    113      */
    114     public enum PhotoPosition {
    115         LEFT,
    116         RIGHT
    117     }
    118     public static final PhotoPosition DEFAULT_PHOTO_POSITION = PhotoPosition.RIGHT;
    119     private PhotoPosition mPhotoPosition = DEFAULT_PHOTO_POSITION;
    120 
    121     // Vertical divider between the call icon and the text.
    122     private boolean mVerticalDividerVisible;
    123     private Drawable mVerticalDividerDrawable;
    124     private int mVerticalDividerWidth;
    125 
    126     // Header layout data
    127     private boolean mHeaderVisible;
    128     private View mHeaderDivider;
    129     private int mHeaderBackgroundHeight;
    130     private TextView mHeaderTextView;
    131 
    132     // The views inside the contact view
    133     private boolean mQuickContactEnabled = true;
    134     private QuickContactBadge mQuickContact;
    135     private ImageView mPhotoView;
    136     private TextView mNameTextView;
    137     private TextView mPhoneticNameTextView;
    138     private DontPressWithParentImageView mCallButton;
    139     private TextView mLabelView;
    140     private TextView mDataView;
    141     private TextView mSnippetView;
    142     private TextView mStatusView;
    143     private TextView mCountView;
    144     private ImageView mPresenceIcon;
    145 
    146     private ColorStateList mSecondaryTextColor;
    147 
    148     private char[] mHighlightedPrefix;
    149 
    150     private int mDefaultPhotoViewSize;
    151     /**
    152      * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
    153      * to align other data in this View.
    154      */
    155     private int mPhotoViewWidth;
    156     /**
    157      * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
    158      */
    159     private int mPhotoViewHeight;
    160 
    161     /**
    162      * Only effective when {@link #mPhotoView} is null.
    163      * When true all the Views on the right side of the photo should have horizontal padding on
    164      * those left assuming there is a photo.
    165      */
    166     private boolean mKeepHorizontalPaddingForPhotoView;
    167     /**
    168      * Only effective when {@link #mPhotoView} is null.
    169      */
    170     private boolean mKeepVerticalPaddingForPhotoView;
    171 
    172     /**
    173      * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
    174      * False indicates those values should be updated before being used in position calculation.
    175      */
    176     private boolean mPhotoViewWidthAndHeightAreReady = false;
    177 
    178     private int mNameTextViewHeight;
    179     private int mPhoneticNameTextViewHeight;
    180     private int mLabelViewHeight;
    181     private int mDataViewHeight;
    182     private int mSnippetTextViewHeight;
    183     private int mStatusTextViewHeight;
    184 
    185     // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
    186     // same row.
    187     private int mLabelAndDataViewMaxHeight;
    188 
    189     private OnClickListener mCallButtonClickListener;
    190     // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
    191     // more efficient for each case or in general, and simplify the whole implementation.
    192     // Note: if we're sure MARQUEE will be used every time, there's no reason to use
    193     // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
    194     // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
    195     // TextView without any modification.
    196     private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
    197     private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
    198 
    199     private boolean mActivatedStateSupported;
    200 
    201     private Rect mBoundsWithoutHeader = new Rect();
    202 
    203     /** A helper used to highlight a prefix in a text field. */
    204     private PrefixHighlighter mPrefixHighligher;
    205     private CharSequence mUnknownNameText;
    206 
    207     /**
    208      * Special class to allow the parent to be pressed without being pressed itself.
    209      * This way the line of a tab can be pressed, but the image itself is not.
    210      */
    211     // TODO: understand this
    212     private static class DontPressWithParentImageView extends ImageView {
    213 
    214         public DontPressWithParentImageView(Context context, AttributeSet attrs) {
    215             super(context, attrs);
    216         }
    217 
    218         @Override
    219         public void setPressed(boolean pressed) {
    220             // If the parent is pressed, do not set to pressed.
    221             if (pressed && ((View) getParent()).isPressed()) {
    222                 return;
    223             }
    224             super.setPressed(pressed);
    225         }
    226     }
    227 
    228     public ContactListItemView(Context context, AttributeSet attrs) {
    229         super(context, attrs);
    230         mContext = context;
    231 
    232         // Read all style values
    233         TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
    234         mPreferredHeight = a.getDimensionPixelSize(
    235                 R.styleable.ContactListItemView_list_item_height, 0);
    236         mActivatedBackgroundDrawable = a.getDrawable(
    237                 R.styleable.ContactListItemView_activated_background);
    238         mHorizontalDividerDrawable = a.getDrawable(
    239                 R.styleable.ContactListItemView_list_item_divider);
    240         mVerticalDividerMargin = a.getDimensionPixelOffset(
    241                 R.styleable.ContactListItemView_list_item_vertical_divider_margin, 0);
    242 
    243         mGapBetweenImageAndText = a.getDimensionPixelOffset(
    244                 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 0);
    245         mGapBetweenLabelAndData = a.getDimensionPixelOffset(
    246                 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0);
    247         mCallButtonPadding = a.getDimensionPixelOffset(
    248                 R.styleable.ContactListItemView_list_item_call_button_padding, 0);
    249         mPresenceIconMargin = a.getDimensionPixelOffset(
    250                 R.styleable.ContactListItemView_list_item_presence_icon_margin, 4);
    251         mPresenceIconSize = a.getDimensionPixelOffset(
    252                 R.styleable.ContactListItemView_list_item_presence_icon_size, 16);
    253         mDefaultPhotoViewSize = a.getDimensionPixelOffset(
    254                 R.styleable.ContactListItemView_list_item_photo_size, 0);
    255         mHeaderTextIndent = a.getDimensionPixelOffset(
    256                 R.styleable.ContactListItemView_list_item_header_text_indent, 0);
    257         mHeaderTextColor = a.getColor(
    258                 R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK);
    259         mHeaderTextSize = a.getDimensionPixelSize(
    260                 R.styleable.ContactListItemView_list_item_header_text_size, 12);
    261         mHeaderBackgroundHeight = a.getDimensionPixelSize(
    262                 R.styleable.ContactListItemView_list_item_header_height, 30);
    263         mHeaderUnderlineHeight = a.getDimensionPixelSize(
    264                 R.styleable.ContactListItemView_list_item_header_underline_height, 1);
    265         mHeaderUnderlineColor = a.getColor(
    266                 R.styleable.ContactListItemView_list_item_header_underline_color, 0);
    267         mTextIndent = a.getDimensionPixelOffset(
    268                 R.styleable.ContactListItemView_list_item_text_indent, 0);
    269         mCountViewTextSize = a.getDimensionPixelSize(
    270                 R.styleable.ContactListItemView_list_item_contacts_count_text_size, 12);
    271         mContactsCountTextColor = a.getColor(
    272                 R.styleable.ContactListItemView_list_item_contacts_count_text_color, Color.BLACK);
    273         mDataViewWidthWeight = a.getInteger(
    274                 R.styleable.ContactListItemView_list_item_data_width_weight, 5);
    275         mLabelViewWidthWeight = a.getInteger(
    276                 R.styleable.ContactListItemView_list_item_label_width_weight, 3);
    277 
    278         setPadding(
    279                 a.getDimensionPixelOffset(
    280                         R.styleable.ContactListItemView_list_item_padding_left, 0),
    281                 a.getDimensionPixelOffset(
    282                         R.styleable.ContactListItemView_list_item_padding_top, 0),
    283                 a.getDimensionPixelOffset(
    284                         R.styleable.ContactListItemView_list_item_padding_right, 0),
    285                 a.getDimensionPixelOffset(
    286                         R.styleable.ContactListItemView_list_item_padding_bottom, 0));
    287 
    288         mPrefixHighligher = new PrefixHighlighter(
    289                 a.getColor(R.styleable.ContactListItemView_list_item_prefix_highlight_color,
    290                         Color.GREEN));
    291         a.recycle();
    292 
    293         a = getContext().obtainStyledAttributes(android.R.styleable.Theme);
    294         mSecondaryTextColor = a.getColorStateList(android.R.styleable.Theme_textColorSecondary);
    295         a.recycle();
    296 
    297         mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
    298 
    299         if (mActivatedBackgroundDrawable != null) {
    300             mActivatedBackgroundDrawable.setCallback(this);
    301         }
    302     }
    303 
    304     /**
    305      * Installs a call button listener.
    306      */
    307     public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) {
    308         mCallButtonClickListener = callButtonClickListener;
    309     }
    310 
    311     public void setUnknownNameText(CharSequence unknownNameText) {
    312         mUnknownNameText = unknownNameText;
    313     }
    314 
    315     public void setQuickContactEnabled(boolean flag) {
    316         mQuickContactEnabled = flag;
    317     }
    318 
    319     @Override
    320     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    321         // We will match parent's width and wrap content vertically, but make sure
    322         // height is no less than listPreferredItemHeight.
    323         final int specWidth = resolveSize(0, widthMeasureSpec);
    324         final int preferredHeight;
    325         if (mHorizontalDividerVisible) {
    326             preferredHeight = mPreferredHeight + mHorizontalDividerHeight;
    327         } else {
    328             preferredHeight = mPreferredHeight;
    329         }
    330 
    331         mNameTextViewHeight = 0;
    332         mPhoneticNameTextViewHeight = 0;
    333         mLabelViewHeight = 0;
    334         mDataViewHeight = 0;
    335         mLabelAndDataViewMaxHeight = 0;
    336         mSnippetTextViewHeight = 0;
    337         mStatusTextViewHeight = 0;
    338 
    339         ensurePhotoViewSize();
    340 
    341         // Width each TextView is able to use.
    342         final int effectiveWidth;
    343         // All the other Views will honor the photo, so available width for them may be shrunk.
    344         if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
    345             effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
    346                     - (mPhotoViewWidth + mGapBetweenImageAndText);
    347         } else {
    348             effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
    349         }
    350 
    351         // Go over all visible text views and measure actual width of each of them.
    352         // Also calculate their heights to get the total height for this entire view.
    353 
    354         if (isVisible(mNameTextView)) {
    355             mNameTextView.measure(
    356                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
    357                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    358             mNameTextViewHeight = mNameTextView.getMeasuredHeight();
    359         }
    360 
    361         if (isVisible(mPhoneticNameTextView)) {
    362             mPhoneticNameTextView.measure(
    363                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
    364                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    365             mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
    366         }
    367 
    368         // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
    369         // we should ellipsize both using appropriate ratio.
    370         final int dataWidth;
    371         final int labelWidth;
    372         if (isVisible(mDataView)) {
    373             if (isVisible(mLabelView)) {
    374                 final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
    375                 dataWidth = ((totalWidth * mDataViewWidthWeight)
    376                         / (mDataViewWidthWeight + mLabelViewWidthWeight));
    377                 labelWidth = ((totalWidth * mLabelViewWidthWeight) /
    378                         (mDataViewWidthWeight + mLabelViewWidthWeight));
    379             } else {
    380                 dataWidth = effectiveWidth;
    381                 labelWidth = 0;
    382             }
    383         } else {
    384             dataWidth = 0;
    385             if (isVisible(mLabelView)) {
    386                 labelWidth = effectiveWidth;
    387             } else {
    388                 labelWidth = 0;
    389             }
    390         }
    391 
    392         if (isVisible(mDataView)) {
    393             mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
    394                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    395             mDataViewHeight = mDataView.getMeasuredHeight();
    396         }
    397 
    398         if (isVisible(mLabelView)) {
    399             // For performance reason we don't want AT_MOST usually, but when the picture is
    400             // on right, we need to use it anyway because mDataView is next to mLabelView.
    401             final int mode = (mPhotoPosition == PhotoPosition.LEFT
    402                     ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST);
    403             mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, mode),
    404                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    405             mLabelViewHeight = mLabelView.getMeasuredHeight();
    406         }
    407         mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
    408 
    409         if (isVisible(mSnippetView)) {
    410             mSnippetView.measure(
    411                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
    412                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    413             mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
    414         }
    415 
    416         // Status view height is the biggest of the text view and the presence icon
    417         if (isVisible(mPresenceIcon)) {
    418             mPresenceIcon.measure(mPresenceIconSize, mPresenceIconSize);
    419             mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
    420         }
    421 
    422         if (isVisible(mStatusView)) {
    423             // Presence and status are in a same row, so status will be affected by icon size.
    424             final int statusWidth;
    425             if (isVisible(mPresenceIcon)) {
    426                 statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
    427                         - mPresenceIconMargin);
    428             } else {
    429                 statusWidth = effectiveWidth;
    430             }
    431             mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
    432                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    433             mStatusTextViewHeight =
    434                     Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
    435         }
    436 
    437         // Calculate height including padding.
    438         int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
    439                 mLabelAndDataViewMaxHeight +
    440                 mSnippetTextViewHeight + mStatusTextViewHeight);
    441 
    442         if (isVisible(mCallButton)) {
    443             mCallButton.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
    444                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    445         }
    446 
    447         // Make sure the height is at least as high as the photo
    448         height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
    449 
    450         // Add horizontal divider height
    451         if (mHorizontalDividerVisible) {
    452             height += mHorizontalDividerHeight;
    453         }
    454 
    455         // Make sure height is at least the preferred height
    456         height = Math.max(height, preferredHeight);
    457 
    458         // Add the height of the header if visible
    459         if (mHeaderVisible) {
    460             mHeaderTextView.measure(
    461                     MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.EXACTLY),
    462                     MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
    463             if (mCountView != null) {
    464                 mCountView.measure(
    465                         MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.AT_MOST),
    466                         MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
    467             }
    468             mHeaderBackgroundHeight = Math.max(mHeaderBackgroundHeight,
    469                     mHeaderTextView.getMeasuredHeight());
    470             height += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
    471         }
    472 
    473         setMeasuredDimension(specWidth, height);
    474     }
    475 
    476     @Override
    477     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    478         final int height = bottom - top;
    479         final int width = right - left;
    480 
    481         // Determine the vertical bounds by laying out the header first.
    482         int topBound = 0;
    483         int bottomBound = height;
    484         int leftBound = getPaddingLeft();
    485         int rightBound = width - getPaddingRight();
    486 
    487         // Put the header in the top of the contact view (Text + underline view)
    488         if (mHeaderVisible) {
    489             mHeaderTextView.layout(leftBound + mHeaderTextIndent,
    490                     0,
    491                     rightBound,
    492                     mHeaderBackgroundHeight);
    493             if (mCountView != null) {
    494                 mCountView.layout(rightBound - mCountView.getMeasuredWidth(),
    495                         0,
    496                         rightBound,
    497                         mHeaderBackgroundHeight);
    498             }
    499             mHeaderDivider.layout(leftBound,
    500                     mHeaderBackgroundHeight,
    501                     rightBound,
    502                     mHeaderBackgroundHeight + mHeaderUnderlineHeight);
    503             topBound += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
    504         }
    505 
    506         // Put horizontal divider at the bottom
    507         if (mHorizontalDividerVisible) {
    508             mHorizontalDividerDrawable.setBounds(
    509                     leftBound,
    510                     height - mHorizontalDividerHeight,
    511                     rightBound,
    512                     height);
    513             bottomBound -= mHorizontalDividerHeight;
    514         }
    515 
    516         mBoundsWithoutHeader.set(0, topBound, width, bottomBound);
    517 
    518         if (mActivatedStateSupported && isActivated()) {
    519             mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
    520         }
    521 
    522         final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
    523         if (mPhotoPosition == PhotoPosition.LEFT) {
    524             // Photo is the left most view. All the other Views should on the right of the photo.
    525             if (photoView != null) {
    526                 // Center the photo vertically
    527                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
    528                 photoView.layout(
    529                         leftBound,
    530                         photoTop,
    531                         leftBound + mPhotoViewWidth,
    532                         photoTop + mPhotoViewHeight);
    533                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
    534             } else if (mKeepHorizontalPaddingForPhotoView) {
    535                 // Draw nothing but keep the padding.
    536                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
    537             }
    538         } else {
    539             // Photo is the right most view. Right bound should be adjusted that way.
    540             if (photoView != null) {
    541                 // Center the photo vertically
    542                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
    543                 photoView.layout(
    544                         rightBound - mPhotoViewWidth,
    545                         photoTop,
    546                         rightBound,
    547                         photoTop + mPhotoViewHeight);
    548                 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
    549             }
    550 
    551             // Add indent between left-most padding and texts.
    552             leftBound += mTextIndent;
    553         }
    554 
    555         // Layout the call button.
    556         rightBound = layoutRightSide(height, topBound, bottomBound, rightBound);
    557 
    558         // Center text vertically
    559         final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
    560                 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
    561         int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
    562 
    563         // Layout all text view and presence icon
    564         // Put name TextView first
    565         if (isVisible(mNameTextView)) {
    566             mNameTextView.layout(leftBound,
    567                     textTopBound,
    568                     rightBound,
    569                     textTopBound + mNameTextViewHeight);
    570             textTopBound += mNameTextViewHeight;
    571         }
    572 
    573         // Presence and status
    574         int statusLeftBound = leftBound;
    575         if (isVisible(mPresenceIcon)) {
    576             int iconWidth = mPresenceIcon.getMeasuredWidth();
    577             mPresenceIcon.layout(
    578                     leftBound,
    579                     textTopBound,
    580                     leftBound + iconWidth,
    581                     textTopBound + mStatusTextViewHeight);
    582             statusLeftBound += (iconWidth + mPresenceIconMargin);
    583         }
    584 
    585         if (isVisible(mStatusView)) {
    586             mStatusView.layout(statusLeftBound,
    587                     textTopBound,
    588                     rightBound,
    589                     textTopBound + mStatusTextViewHeight);
    590         }
    591 
    592         if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
    593             textTopBound += mStatusTextViewHeight;
    594         }
    595 
    596         // Rest of text views
    597         int dataLeftBound = leftBound;
    598         if (isVisible(mPhoneticNameTextView)) {
    599             mPhoneticNameTextView.layout(leftBound,
    600                     textTopBound,
    601                     rightBound,
    602                     textTopBound + mPhoneticNameTextViewHeight);
    603             textTopBound += mPhoneticNameTextViewHeight;
    604         }
    605 
    606         // Label and Data align bottom.
    607         if (isVisible(mLabelView)) {
    608             if (mPhotoPosition == PhotoPosition.LEFT) {
    609                 // When photo is on left, label is placed on the right edge of the list item.
    610                 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
    611                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
    612                         rightBound,
    613                         textTopBound + mLabelAndDataViewMaxHeight);
    614                 rightBound -= mLabelView.getMeasuredWidth();
    615             } else {
    616                 // When photo is on right, label is placed on the left of data view.
    617                 dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
    618                 mLabelView.layout(leftBound,
    619                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
    620                         dataLeftBound,
    621                         textTopBound + mLabelAndDataViewMaxHeight);
    622                 dataLeftBound += mGapBetweenLabelAndData;
    623             }
    624         }
    625 
    626         if (isVisible(mDataView)) {
    627             mDataView.layout(dataLeftBound,
    628                     textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
    629                     rightBound,
    630                     textTopBound + mLabelAndDataViewMaxHeight);
    631         }
    632         if (isVisible(mLabelView) || isVisible(mDataView)) {
    633             textTopBound += mLabelAndDataViewMaxHeight;
    634         }
    635 
    636         if (isVisible(mSnippetView)) {
    637             mSnippetView.layout(leftBound,
    638                     textTopBound,
    639                     rightBound,
    640                     textTopBound + mSnippetTextViewHeight);
    641         }
    642     }
    643 
    644     /**
    645      * Performs layout of the right side of the view
    646      *
    647      * @return new right boundary
    648      */
    649     protected int layoutRightSide(int height, int topBound, int bottomBound, int rightBound) {
    650         // Put call button and vertical divider
    651         if (isVisible(mCallButton)) {
    652             int buttonWidth = mCallButton.getMeasuredWidth();
    653             rightBound -= buttonWidth;
    654             mCallButton.layout(
    655                     rightBound,
    656                     topBound,
    657                     rightBound + buttonWidth,
    658                     height - mHorizontalDividerHeight);
    659             mVerticalDividerVisible = true;
    660             ensureVerticalDivider();
    661             rightBound -= mVerticalDividerWidth;
    662             mVerticalDividerDrawable.setBounds(
    663                     rightBound,
    664                     topBound + mVerticalDividerMargin,
    665                     rightBound + mVerticalDividerWidth,
    666                     height - mVerticalDividerMargin);
    667         } else {
    668             mVerticalDividerVisible = false;
    669         }
    670 
    671         return rightBound;
    672     }
    673 
    674     @Override
    675     public void adjustListItemSelectionBounds(Rect bounds) {
    676         bounds.top += mBoundsWithoutHeader.top;
    677         bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
    678         bounds.left += mSelectionBoundsMarginLeft;
    679         bounds.right -= mSelectionBoundsMarginRight;
    680     }
    681 
    682     protected boolean isVisible(View view) {
    683         return view != null && view.getVisibility() == View.VISIBLE;
    684     }
    685 
    686     /**
    687      * Loads the drawable for the vertical divider if it has not yet been loaded.
    688      */
    689     private void ensureVerticalDivider() {
    690         if (mVerticalDividerDrawable == null) {
    691             mVerticalDividerDrawable = mContext.getResources().getDrawable(
    692                     R.drawable.divider_vertical_dark);
    693             mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth();
    694         }
    695     }
    696 
    697     /**
    698      * Extracts width and height from the style
    699      */
    700     private void ensurePhotoViewSize() {
    701         if (!mPhotoViewWidthAndHeightAreReady) {
    702             if (mQuickContactEnabled) {
    703                 TypedArray a = mContext.obtainStyledAttributes(null,
    704                         com.android.internal.R.styleable.ViewGroup_Layout,
    705                         QUICK_CONTACT_BADGE_STYLE, 0);
    706                 mPhotoViewWidth = a.getLayoutDimension(
    707                         android.R.styleable.ViewGroup_Layout_layout_width,
    708                         ViewGroup.LayoutParams.WRAP_CONTENT);
    709                 mPhotoViewHeight = a.getLayoutDimension(
    710                         android.R.styleable.ViewGroup_Layout_layout_height,
    711                         ViewGroup.LayoutParams.WRAP_CONTENT);
    712                 a.recycle();
    713             } else if (mPhotoView != null) {
    714                 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
    715             } else {
    716                 final int defaultPhotoViewSize = getDefaultPhotoViewSize();
    717                 mPhotoViewWidth = mKeepHorizontalPaddingForPhotoView ? defaultPhotoViewSize : 0;
    718                 mPhotoViewHeight = mKeepVerticalPaddingForPhotoView ? defaultPhotoViewSize : 0;
    719             }
    720 
    721             mPhotoViewWidthAndHeightAreReady = true;
    722         }
    723     }
    724 
    725     protected void setDefaultPhotoViewSize(int pixels) {
    726         mDefaultPhotoViewSize = pixels;
    727     }
    728 
    729     protected int getDefaultPhotoViewSize() {
    730         return mDefaultPhotoViewSize;
    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         if (mVerticalDividerVisible) {
    763             mVerticalDividerDrawable.draw(canvas);
    764         }
    765 
    766         super.dispatchDraw(canvas);
    767     }
    768 
    769     /**
    770      * Sets the flag that determines whether a divider should drawn at the bottom
    771      * of the view.
    772      */
    773     public void setDividerVisible(boolean visible) {
    774         mHorizontalDividerVisible = visible;
    775     }
    776 
    777     /**
    778      * Sets section header or makes it invisible if the title is null.
    779      */
    780     public void setSectionHeader(String title) {
    781         if (!TextUtils.isEmpty(title)) {
    782             if (mHeaderTextView == null) {
    783                 mHeaderTextView = new TextView(mContext);
    784                 mHeaderTextView.setTextColor(mHeaderTextColor);
    785                 mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize);
    786                 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
    787                 mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL);
    788                 addView(mHeaderTextView);
    789             }
    790             if (mHeaderDivider == null) {
    791                 mHeaderDivider = new View(mContext);
    792                 mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor);
    793                 addView(mHeaderDivider);
    794             }
    795             setMarqueeText(mHeaderTextView, title);
    796             mHeaderTextView.setVisibility(View.VISIBLE);
    797             mHeaderDivider.setVisibility(View.VISIBLE);
    798             mHeaderTextView.setAllCaps(true);
    799             mHeaderVisible = true;
    800         } else {
    801             if (mHeaderTextView != null) {
    802                 mHeaderTextView.setVisibility(View.GONE);
    803             }
    804             if (mHeaderDivider != null) {
    805                 mHeaderDivider.setVisibility(View.GONE);
    806             }
    807             mHeaderVisible = false;
    808         }
    809     }
    810 
    811     /**
    812      * Returns the quick contact badge, creating it if necessary.
    813      */
    814     public QuickContactBadge getQuickContact() {
    815         if (!mQuickContactEnabled) {
    816             throw new IllegalStateException("QuickContact is disabled for this view");
    817         }
    818         if (mQuickContact == null) {
    819             mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE);
    820             if (mNameTextView != null) {
    821                 mQuickContact.setContentDescription(mContext.getString(
    822                         R.string.description_quick_contact_for, mNameTextView.getText()));
    823             }
    824 
    825             addView(mQuickContact);
    826             mPhotoViewWidthAndHeightAreReady = false;
    827         }
    828         return mQuickContact;
    829     }
    830 
    831     /**
    832      * Returns the photo view, creating it if necessary.
    833      */
    834     public ImageView getPhotoView() {
    835         if (mPhotoView == null) {
    836             if (mQuickContactEnabled) {
    837                 mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE);
    838             } else {
    839                 mPhotoView = new ImageView(mContext);
    840             }
    841             // Quick contact style used above will set a background - remove it
    842             mPhotoView.setBackgroundDrawable(null);
    843             addView(mPhotoView);
    844             mPhotoViewWidthAndHeightAreReady = false;
    845         }
    846         return mPhotoView;
    847     }
    848 
    849     /**
    850      * Removes the photo view.
    851      */
    852     public void removePhotoView() {
    853         removePhotoView(false, true);
    854     }
    855 
    856     /**
    857      * Removes the photo view.
    858      *
    859      * @param keepHorizontalPadding True means data on the right side will have
    860      *            padding on left, pretending there is still a photo view.
    861      * @param keepVerticalPadding True means the View will have some height
    862      *            enough for accommodating a photo view.
    863      */
    864     public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
    865         mPhotoViewWidthAndHeightAreReady = false;
    866         mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
    867         mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
    868         if (mPhotoView != null) {
    869             removeView(mPhotoView);
    870             mPhotoView = null;
    871         }
    872         if (mQuickContact != null) {
    873             removeView(mQuickContact);
    874             mQuickContact = null;
    875         }
    876     }
    877 
    878     /**
    879      * Sets a word prefix that will be highlighted if encountered in fields like
    880      * name and search snippet.
    881      * <p>
    882      * NOTE: must be all upper-case
    883      */
    884     public void setHighlightedPrefix(char[] upperCasePrefix) {
    885         mHighlightedPrefix = upperCasePrefix;
    886     }
    887 
    888     /**
    889      * Returns the text view for the contact name, creating it if necessary.
    890      */
    891     public TextView getNameTextView() {
    892         if (mNameTextView == null) {
    893             mNameTextView = new TextView(mContext);
    894             mNameTextView.setSingleLine(true);
    895             mNameTextView.setEllipsize(getTextEllipsis());
    896             mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
    897             // Manually call setActivated() since this view may be added after the first
    898             // setActivated() call toward this whole item view.
    899             mNameTextView.setActivated(isActivated());
    900             mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
    901             addView(mNameTextView);
    902         }
    903         return mNameTextView;
    904     }
    905 
    906     /**
    907      * Adds a call button using the supplied arguments as an id and tag.
    908      */
    909     public void showCallButton(int id, int tag) {
    910         if (mCallButton == null) {
    911             mCallButton = new DontPressWithParentImageView(mContext, null);
    912             mCallButton.setId(id);
    913             mCallButton.setOnClickListener(mCallButtonClickListener);
    914             mCallButton.setBackgroundResource(R.drawable.call_background);
    915             mCallButton.setImageResource(android.R.drawable.sym_action_call);
    916             mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0);
    917             mCallButton.setScaleType(ScaleType.CENTER);
    918             addView(mCallButton);
    919         }
    920 
    921         mCallButton.setTag(tag);
    922         mCallButton.setVisibility(View.VISIBLE);
    923     }
    924 
    925     public void hideCallButton() {
    926         if (mCallButton != null) {
    927             mCallButton.setVisibility(View.GONE);
    928         }
    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             addView(mPhoneticNameTextView);
    958         }
    959         return mPhoneticNameTextView;
    960     }
    961 
    962     /**
    963      * Adds or updates a text view for the data label.
    964      */
    965     public void setLabel(CharSequence text) {
    966         if (TextUtils.isEmpty(text)) {
    967             if (mLabelView != null) {
    968                 mLabelView.setVisibility(View.GONE);
    969             }
    970         } else {
    971             getLabelView();
    972             setMarqueeText(mLabelView, text);
    973             mLabelView.setVisibility(VISIBLE);
    974         }
    975     }
    976 
    977     /**
    978      * Returns the text view for the data label, creating it if necessary.
    979      */
    980     public TextView getLabelView() {
    981         if (mLabelView == null) {
    982             mLabelView = new TextView(mContext);
    983             mLabelView.setSingleLine(true);
    984             mLabelView.setEllipsize(getTextEllipsis());
    985             mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
    986             if (mPhotoPosition == PhotoPosition.LEFT) {
    987                 mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mCountViewTextSize);
    988                 mLabelView.setAllCaps(true);
    989                 mLabelView.setGravity(Gravity.RIGHT);
    990             } else {
    991                 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
    992             }
    993             mLabelView.setActivated(isActivated());
    994             addView(mLabelView);
    995         }
    996         return mLabelView;
    997     }
    998 
    999     /**
   1000      * Adds or updates a text view for the data element.
   1001      */
   1002     public void setData(char[] text, int size) {
   1003         if (text == null || size == 0) {
   1004             if (mDataView != null) {
   1005                 mDataView.setVisibility(View.GONE);
   1006             }
   1007         } else {
   1008             getDataView();
   1009             setMarqueeText(mDataView, text, size);
   1010             mDataView.setVisibility(VISIBLE);
   1011         }
   1012     }
   1013 
   1014     private void setMarqueeText(TextView textView, char[] text, int size) {
   1015         if (getTextEllipsis() == TruncateAt.MARQUEE) {
   1016             setMarqueeText(textView, new String(text, 0, size));
   1017         } else {
   1018             textView.setText(text, 0, size);
   1019         }
   1020     }
   1021 
   1022     private void setMarqueeText(TextView textView, CharSequence text) {
   1023         if (getTextEllipsis() == TruncateAt.MARQUEE) {
   1024             // To show MARQUEE correctly (with END effect during non-active state), we need
   1025             // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
   1026             final SpannableString spannable = new SpannableString(text);
   1027             spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
   1028                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1029             textView.setText(spannable);
   1030         } else {
   1031             textView.setText(text);
   1032         }
   1033     }
   1034 
   1035     /**
   1036      * Returns the text view for the data text, creating it if necessary.
   1037      */
   1038     public TextView getDataView() {
   1039         if (mDataView == null) {
   1040             mDataView = new TextView(mContext);
   1041             mDataView.setSingleLine(true);
   1042             mDataView.setEllipsize(getTextEllipsis());
   1043             mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
   1044             mDataView.setActivated(isActivated());
   1045             addView(mDataView);
   1046         }
   1047         return mDataView;
   1048     }
   1049 
   1050     /**
   1051      * Adds or updates a text view for the search snippet.
   1052      */
   1053     public void setSnippet(String text) {
   1054         if (TextUtils.isEmpty(text)) {
   1055             if (mSnippetView != null) {
   1056                 mSnippetView.setVisibility(View.GONE);
   1057             }
   1058         } else {
   1059             mPrefixHighligher.setText(getSnippetView(), text, mHighlightedPrefix);
   1060             mSnippetView.setVisibility(VISIBLE);
   1061         }
   1062     }
   1063 
   1064     /**
   1065      * Returns the text view for the search snippet, creating it if necessary.
   1066      */
   1067     public TextView getSnippetView() {
   1068         if (mSnippetView == null) {
   1069             mSnippetView = new TextView(mContext);
   1070             mSnippetView.setSingleLine(true);
   1071             mSnippetView.setEllipsize(getTextEllipsis());
   1072             mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
   1073             mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
   1074             mSnippetView.setActivated(isActivated());
   1075             addView(mSnippetView);
   1076         }
   1077         return mSnippetView;
   1078     }
   1079 
   1080     /**
   1081      * Returns the text view for the status, creating it if necessary.
   1082      */
   1083     public TextView getStatusView() {
   1084         if (mStatusView == null) {
   1085             mStatusView = new TextView(mContext);
   1086             mStatusView.setSingleLine(true);
   1087             mStatusView.setEllipsize(getTextEllipsis());
   1088             mStatusView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
   1089             mStatusView.setTextColor(mSecondaryTextColor);
   1090             mStatusView.setActivated(isActivated());
   1091             addView(mStatusView);
   1092         }
   1093         return mStatusView;
   1094     }
   1095 
   1096     /**
   1097      * Returns the text view for the contacts count, creating it if necessary.
   1098      */
   1099     public TextView getCountView() {
   1100         if (mCountView == null) {
   1101             mCountView = new TextView(mContext);
   1102             mCountView.setSingleLine(true);
   1103             mCountView.setEllipsize(getTextEllipsis());
   1104             mCountView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
   1105             mCountView.setTextColor(R.color.contact_count_text_color);
   1106             addView(mCountView);
   1107         }
   1108         return mCountView;
   1109     }
   1110 
   1111     /**
   1112      * Adds or updates a text view for the contacts count.
   1113      */
   1114     public void setCountView(CharSequence text) {
   1115         if (TextUtils.isEmpty(text)) {
   1116             if (mCountView != null) {
   1117                 mCountView.setVisibility(View.GONE);
   1118             }
   1119         } else {
   1120             getCountView();
   1121             setMarqueeText(mCountView, text);
   1122             mCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize);
   1123             mCountView.setGravity(Gravity.CENTER_VERTICAL);
   1124             mCountView.setTextColor(mContactsCountTextColor);
   1125             mCountView.setVisibility(VISIBLE);
   1126         }
   1127     }
   1128 
   1129     /**
   1130      * Adds or updates a text view for the status.
   1131      */
   1132     public void setStatus(CharSequence text) {
   1133         if (TextUtils.isEmpty(text)) {
   1134             if (mStatusView != null) {
   1135                 mStatusView.setVisibility(View.GONE);
   1136             }
   1137         } else {
   1138             getStatusView();
   1139             setMarqueeText(mStatusView, text);
   1140             mStatusView.setVisibility(VISIBLE);
   1141         }
   1142     }
   1143 
   1144     /**
   1145      * Adds or updates the presence icon view.
   1146      */
   1147     public void setPresence(Drawable icon) {
   1148         if (icon != null) {
   1149             if (mPresenceIcon == null) {
   1150                 mPresenceIcon = new ImageView(mContext);
   1151                 addView(mPresenceIcon);
   1152             }
   1153             mPresenceIcon.setImageDrawable(icon);
   1154             mPresenceIcon.setScaleType(ScaleType.CENTER);
   1155             mPresenceIcon.setVisibility(View.VISIBLE);
   1156         } else {
   1157             if (mPresenceIcon != null) {
   1158                 mPresenceIcon.setVisibility(View.GONE);
   1159             }
   1160         }
   1161     }
   1162 
   1163     private TruncateAt getTextEllipsis() {
   1164         return TruncateAt.MARQUEE;
   1165     }
   1166 
   1167     public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
   1168         CharSequence name = cursor.getString(nameColumnIndex);
   1169         if (!TextUtils.isEmpty(name)) {
   1170             name = mPrefixHighligher.apply(name, mHighlightedPrefix);
   1171         } else {
   1172             name = mUnknownNameText;
   1173         }
   1174         setMarqueeText(getNameTextView(), name);
   1175 
   1176         // Since the quick contact content description is derived from the display name and there is
   1177         // no guarantee that when the quick contact is initialized the display name is already set,
   1178         // do it here too.
   1179         if (mQuickContact != null) {
   1180             mQuickContact.setContentDescription(mContext.getString(
   1181                     R.string.description_quick_contact_for, mNameTextView.getText()));
   1182         }
   1183     }
   1184 
   1185     public void hideDisplayName() {
   1186         if (mNameTextView != null) {
   1187             removeView(mNameTextView);
   1188             mNameTextView = null;
   1189         }
   1190     }
   1191 
   1192     public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
   1193         cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
   1194         int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
   1195         if (phoneticNameSize != 0) {
   1196             setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
   1197         } else {
   1198             setPhoneticName(null, 0);
   1199         }
   1200     }
   1201 
   1202     public void hidePhoneticName() {
   1203         if (mPhoneticNameTextView != null) {
   1204             removeView(mPhoneticNameTextView);
   1205             mPhoneticNameTextView = null;
   1206         }
   1207     }
   1208 
   1209     /**
   1210      * Sets the proper icon (star or presence or nothing) and/or status message.
   1211      */
   1212     public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
   1213             int contactStatusColumnIndex) {
   1214         Drawable icon = null;
   1215         int presence = 0;
   1216         if (!cursor.isNull(presenceColumnIndex)) {
   1217             presence = cursor.getInt(presenceColumnIndex);
   1218             icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
   1219         }
   1220         setPresence(icon);
   1221 
   1222         String statusMessage = null;
   1223         if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
   1224             statusMessage = cursor.getString(contactStatusColumnIndex);
   1225         }
   1226         // If there is no status message from the contact, but there was a presence value, then use
   1227         // the default status message string
   1228         if (statusMessage == null && presence != 0) {
   1229             statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
   1230         }
   1231         setStatus(statusMessage);
   1232     }
   1233 
   1234     /**
   1235      * Shows search snippet.
   1236      */
   1237     public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
   1238         if (cursor.getColumnCount() <= summarySnippetColumnIndex) {
   1239             setSnippet(null);
   1240             return;
   1241         }
   1242         String snippet;
   1243         String columnContent = cursor.getString(summarySnippetColumnIndex);
   1244 
   1245         // Do client side snippeting if provider didn't do it
   1246         Bundle extras = cursor.getExtras();
   1247         if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
   1248             int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
   1249 
   1250             snippet = ContactsContract.snippetize(columnContent,
   1251                     displayNameIndex < 0 ? null : cursor.getString(displayNameIndex),
   1252                             extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY),
   1253                             DefaultContactListAdapter.SNIPPET_START_MATCH,
   1254                             DefaultContactListAdapter.SNIPPET_END_MATCH,
   1255                             DefaultContactListAdapter.SNIPPET_ELLIPSIS,
   1256                             DefaultContactListAdapter.SNIPPET_MAX_TOKENS);
   1257         } else {
   1258             snippet = columnContent;
   1259         }
   1260 
   1261         if (snippet != null) {
   1262             int from = 0;
   1263             int to = snippet.length();
   1264             int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
   1265             if (start == -1) {
   1266                 snippet = null;
   1267             } else {
   1268                 int firstNl = snippet.lastIndexOf('\n', start);
   1269                 if (firstNl != -1) {
   1270                     from = firstNl + 1;
   1271                 }
   1272                 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
   1273                 if (end != -1) {
   1274                     int lastNl = snippet.indexOf('\n', end);
   1275                     if (lastNl != -1) {
   1276                         to = lastNl;
   1277                     }
   1278                 }
   1279 
   1280                 StringBuilder sb = new StringBuilder();
   1281                 for (int i = from; i < to; i++) {
   1282                     char c = snippet.charAt(i);
   1283                     if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
   1284                             c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
   1285                         sb.append(c);
   1286                     }
   1287                 }
   1288                 snippet = sb.toString();
   1289             }
   1290         }
   1291         setSnippet(snippet);
   1292     }
   1293 
   1294     /**
   1295      * Shows data element (e.g. phone number).
   1296      */
   1297     public void showData(Cursor cursor, int dataColumnIndex) {
   1298         cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
   1299         setData(mDataBuffer.data, mDataBuffer.sizeCopied);
   1300     }
   1301 
   1302     public void setActivatedStateSupported(boolean flag) {
   1303         this.mActivatedStateSupported = flag;
   1304     }
   1305 
   1306     @Override
   1307     public void requestLayout() {
   1308         // We will assume that once measured this will not need to resize
   1309         // itself, so there is no need to pass the layout request to the parent
   1310         // view (ListView).
   1311         forceLayout();
   1312     }
   1313 
   1314     public void setPhotoPosition(PhotoPosition photoPosition) {
   1315         mPhotoPosition = photoPosition;
   1316     }
   1317 
   1318     public PhotoPosition getPhotoPosition() {
   1319         return mPhotoPosition;
   1320     }
   1321 
   1322     /**
   1323      * Specifies left and right margin for selection bounds. See also
   1324      * {@link #adjustListItemSelectionBounds(Rect)}.
   1325      */
   1326     public void setSelectionBoundsHorizontalMargin(int left, int right) {
   1327         mSelectionBoundsMarginLeft = left;
   1328         mSelectionBoundsMarginRight = right;
   1329     }
   1330 }
   1331