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