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