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