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