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