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