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