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