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.quantum_ic_videocam_vd_theme_24);
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.quantum_ic_star_vd_theme_24,
1035                        getContext().getTheme()));
1036        headerImageView.setImageTintList(ColorStateList.valueOf(getResources()
1037                .getColor(R.color.material_star_pink)));
1038        headerImageView.setContentDescription(
1039                getContext().getString(R.string.contactsFavoritesLabel));
1040        headerImageView.setVisibility(View.VISIBLE);
1041        addView(headerImageView);
1042    }
1043
1044    public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
1045        mIsSectionHeaderEnabled = isSectionHeaderEnabled;
1046    }
1047
1048    /**
1049     * Returns the quick contact badge, creating it if necessary.
1050     */
1051    public QuickContactBadge getQuickContact() {
1052        if (!mQuickContactEnabled) {
1053            throw new IllegalStateException("QuickContact is disabled for this view");
1054        }
1055        if (mQuickContact == null) {
1056            mQuickContact = new QuickContactBadge(getContext());
1057            if (CompatUtils.isLollipopCompatible()) {
1058                mQuickContact.setOverlay(null);
1059            }
1060            mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
1061            if (mNameTextView != null) {
1062                mQuickContact.setContentDescription(getContext().getString(
1063                        R.string.description_quick_contact_for, mNameTextView.getText()));
1064            }
1065
1066            addView(mQuickContact);
1067            mPhotoViewWidthAndHeightAreReady = false;
1068        }
1069        return mQuickContact;
1070    }
1071
1072    /**
1073     * Returns the photo view, creating it if necessary.
1074     */
1075    public ImageView getPhotoView() {
1076        if (mPhotoView == null) {
1077            mPhotoView = new ImageView(getContext());
1078            mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
1079            // Quick contact style used above will set a background - remove it
1080            mPhotoView.setBackground(null);
1081            addView(mPhotoView);
1082            mPhotoViewWidthAndHeightAreReady = false;
1083        }
1084        return mPhotoView;
1085    }
1086
1087    /**
1088     * Removes the photo view.
1089     */
1090    public void removePhotoView() {
1091        removePhotoView(false, true);
1092    }
1093
1094    /**
1095     * Removes the photo view.
1096     *
1097     * @param keepHorizontalPadding True means data on the right side will have
1098     *            padding on left, pretending there is still a photo view.
1099     * @param keepVerticalPadding True means the View will have some height
1100     *            enough for accommodating a photo view.
1101     */
1102    public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
1103        mPhotoViewWidthAndHeightAreReady = false;
1104        mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
1105        mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
1106        if (mPhotoView != null) {
1107            removeView(mPhotoView);
1108            mPhotoView = null;
1109        }
1110        if (mQuickContact != null) {
1111            removeView(mQuickContact);
1112            mQuickContact = null;
1113        }
1114    }
1115
1116    /**
1117     * Sets a word prefix that will be highlighted if encountered in fields like
1118     * name and search snippet. This will disable the mask highlighting for names.
1119     * <p>
1120     * NOTE: must be all upper-case
1121     */
1122    public void setHighlightedPrefix(String upperCasePrefix) {
1123        mHighlightedPrefix = upperCasePrefix;
1124    }
1125
1126    /**
1127     * Clears previously set highlight sequences for the view.
1128     */
1129    public void clearHighlightSequences() {
1130        mNameHighlightSequence.clear();
1131        mNumberHighlightSequence.clear();
1132        mHighlightedPrefix = null;
1133    }
1134
1135    /**
1136     * Adds a highlight sequence to the name highlighter.
1137     * @param start The start position of the highlight sequence.
1138     * @param end The end position of the highlight sequence.
1139     */
1140    public void addNameHighlightSequence(int start, int end) {
1141        mNameHighlightSequence.add(new HighlightSequence(start, end));
1142    }
1143
1144    /**
1145     * Adds a highlight sequence to the number highlighter.
1146     * @param start The start position of the highlight sequence.
1147     * @param end The end position of the highlight sequence.
1148     */
1149    public void addNumberHighlightSequence(int start, int end) {
1150        mNumberHighlightSequence.add(new HighlightSequence(start, end));
1151    }
1152
1153    /**
1154     * Returns the text view for the contact name, creating it if necessary.
1155     */
1156    public TextView getNameTextView() {
1157        if (mNameTextView == null) {
1158            mNameTextView = new TextView(getContext());
1159            mNameTextView.setSingleLine(true);
1160            mNameTextView.setEllipsize(getTextEllipsis());
1161            mNameTextView.setTextColor(ResourcesCompat.getColorStateList(getResources(),
1162                    R.color.contact_list_name_text_color, getContext().getTheme()));
1163            mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
1164            // Manually call setActivated() since this view may be added after the first
1165            // setActivated() call toward this whole item view.
1166            mNameTextView.setActivated(isActivated());
1167            mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
1168            mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1169            mNameTextView.setId(R.id.cliv_name_textview);
1170            if (CompatUtils.isLollipopCompatible()) {
1171                mNameTextView.setElegantTextHeight(false);
1172            }
1173            addView(mNameTextView);
1174        }
1175        return mNameTextView;
1176    }
1177
1178    /**
1179     * Adds or updates a text view for the phonetic name.
1180     */
1181    public void setPhoneticName(char[] text, int size) {
1182        if (text == null || size == 0) {
1183            if (mPhoneticNameTextView != null) {
1184                mPhoneticNameTextView.setVisibility(View.GONE);
1185            }
1186        } else {
1187            getPhoneticNameTextView();
1188            setMarqueeText(mPhoneticNameTextView, text, size);
1189            mPhoneticNameTextView.setVisibility(VISIBLE);
1190        }
1191    }
1192
1193    /**
1194     * Returns the text view for the phonetic name, creating it if necessary.
1195     */
1196    public TextView getPhoneticNameTextView() {
1197        if (mPhoneticNameTextView == null) {
1198            mPhoneticNameTextView = new TextView(getContext());
1199            mPhoneticNameTextView.setSingleLine(true);
1200            mPhoneticNameTextView.setEllipsize(getTextEllipsis());
1201            mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1202            mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1203            mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
1204            mPhoneticNameTextView.setActivated(isActivated());
1205            mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview);
1206            addView(mPhoneticNameTextView);
1207        }
1208        return mPhoneticNameTextView;
1209    }
1210
1211    /**
1212     * Adds or updates a text view for the data label.
1213     */
1214    public void setLabel(CharSequence text) {
1215        if (TextUtils.isEmpty(text)) {
1216            if (mLabelView != null) {
1217                mLabelView.setVisibility(View.GONE);
1218            }
1219        } else {
1220            getLabelView();
1221            setMarqueeText(mLabelView, text);
1222            mLabelView.setVisibility(VISIBLE);
1223        }
1224    }
1225
1226    /**
1227     * Returns the text view for the data label, creating it if necessary.
1228     */
1229    public TextView getLabelView() {
1230        if (mLabelView == null) {
1231            mLabelView = new TextView(getContext());
1232            mLabelView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
1233                    LayoutParams.WRAP_CONTENT));
1234
1235            mLabelView.setSingleLine(true);
1236            mLabelView.setEllipsize(getTextEllipsis());
1237            mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
1238            if (mPhotoPosition == PhotoPosition.LEFT) {
1239                mLabelView.setAllCaps(true);
1240            } else {
1241                mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
1242            }
1243            mLabelView.setActivated(isActivated());
1244            mLabelView.setId(R.id.cliv_label_textview);
1245            addView(mLabelView);
1246        }
1247        return mLabelView;
1248    }
1249
1250    /**
1251     * Adds or updates a text view for the data element.
1252     */
1253    public void setData(char[] text, int size) {
1254        if (text == null || size == 0) {
1255            if (mDataView != null) {
1256                mDataView.setVisibility(View.GONE);
1257            }
1258        } else {
1259            getDataView();
1260            setMarqueeText(mDataView, text, size);
1261            mDataView.setVisibility(VISIBLE);
1262        }
1263    }
1264
1265    /**
1266     * Sets phone number for a list item. This takes care of number highlighting if the highlight
1267     * mask exists.
1268     */
1269    public void setPhoneNumber(String text, String countryIso) {
1270        if (text == null) {
1271            if (mDataView != null) {
1272                mDataView.setVisibility(View.GONE);
1273            }
1274        } else {
1275            getDataView();
1276
1277            // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
1278            // mDataView. Make sure that determination of the highlight sequences are done only
1279            // after number formatting.
1280
1281            // Sets phone number texts for display after highlighting it, if applicable.
1282            // CharSequence textToSet = text;
1283            final SpannableString textToSet = new SpannableString(text);
1284
1285            if (mNumberHighlightSequence.size() != 0) {
1286                final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
1287                mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start,
1288                        highlightSequence.end);
1289            }
1290
1291            setMarqueeText(mDataView, textToSet);
1292            mDataView.setVisibility(VISIBLE);
1293
1294            // We have a phone number as "mDataView" so make it always LTR and VIEW_START
1295            mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
1296            mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1297        }
1298    }
1299
1300    private void setMarqueeText(TextView textView, char[] text, int size) {
1301        if (getTextEllipsis() == TruncateAt.MARQUEE) {
1302            setMarqueeText(textView, new String(text, 0, size));
1303        } else {
1304            textView.setText(text, 0, size);
1305        }
1306    }
1307
1308    private void setMarqueeText(TextView textView, CharSequence text) {
1309        if (getTextEllipsis() == TruncateAt.MARQUEE) {
1310            // To show MARQUEE correctly (with END effect during non-active state), we need
1311            // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
1312            final SpannableString spannable = new SpannableString(text);
1313            spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
1314                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1315            textView.setText(spannable);
1316        } else {
1317            textView.setText(text);
1318        }
1319    }
1320
1321    /**
1322     * Returns the {@link AppCompatCheckBox} view, creating it if necessary.
1323     */
1324    public AppCompatCheckBox getCheckBox() {
1325        if (mCheckBox == null) {
1326            mCheckBox = new AppCompatCheckBox(getContext());
1327            // Make non-focusable, so the rest of the ContactListItemView can be clicked.
1328            mCheckBox.setFocusable(false);
1329            addView(mCheckBox);
1330        }
1331        return mCheckBox;
1332    }
1333
1334    /**
1335     * Returns the {@link AppCompatImageButton} delete button, creating it if necessary.
1336     */
1337    public AppCompatImageButton getDeleteImageButton(
1338            final MultiSelectEntryContactListAdapter.DeleteContactListener listener,
1339            final int position) {
1340        if (mDeleteImageButton == null) {
1341            mDeleteImageButton = new AppCompatImageButton(getContext());
1342            mDeleteImageButton.setImageResource(R.drawable.quantum_ic_cancel_vd_theme_24);
1343            mDeleteImageButton.setScaleType(ScaleType.CENTER);
1344            mDeleteImageButton.setBackgroundColor(Color.TRANSPARENT);
1345            mDeleteImageButton.setContentDescription(
1346                    getResources().getString(R.string.description_delete_contact));
1347            if (CompatUtils. isLollipopCompatible()) {
1348                final TypedValue typedValue = new TypedValue();
1349                getContext().getTheme().resolveAttribute(
1350                        android.R.attr.selectableItemBackgroundBorderless, typedValue, true);
1351                mDeleteImageButton.setBackgroundResource(typedValue.resourceId);
1352            }
1353            addView(mDeleteImageButton);
1354        }
1355        // Reset onClickListener because after reloading the view, position might be changed.
1356        mDeleteImageButton.setOnClickListener(new OnClickListener() {
1357            @Override
1358            public void onClick(View v) {
1359                // Inform the adapter that delete icon was clicked.
1360                if (listener != null) {
1361                    listener.onContactDeleteClicked(position);
1362                }
1363            }
1364        });
1365        return mDeleteImageButton;
1366    }
1367
1368    /**
1369     * Returns the text view for the data text, creating it if necessary.
1370     */
1371    public TextView getDataView() {
1372        if (mDataView == null) {
1373            mDataView = new TextView(getContext());
1374            mDataView.setSingleLine(true);
1375            mDataView.setEllipsize(getTextEllipsis());
1376            mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
1377            mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1378            mDataView.setActivated(isActivated());
1379            mDataView.setId(R.id.cliv_data_view);
1380            if (CompatUtils.isLollipopCompatible()) {
1381                mDataView.setElegantTextHeight(false);
1382            }
1383            addView(mDataView);
1384        }
1385        return mDataView;
1386    }
1387
1388    /**
1389     * Adds or updates a text view for the search snippet.
1390     */
1391    public void setSnippet(String text) {
1392        if (TextUtils.isEmpty(text)) {
1393            if (mSnippetView != null) {
1394                mSnippetView.setVisibility(View.GONE);
1395            }
1396        } else {
1397            mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
1398            mSnippetView.setVisibility(VISIBLE);
1399            if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
1400                // Give the text-to-speech engine a hint that it's a phone number
1401                mSnippetView.setContentDescription(
1402                        PhoneNumberUtilsCompat.createTtsSpannable(text));
1403            } else {
1404                mSnippetView.setContentDescription(null);
1405            }
1406        }
1407    }
1408
1409    /**
1410     * Returns the text view for the search snippet, creating it if necessary.
1411     */
1412    public TextView getSnippetView() {
1413        if (mSnippetView == null) {
1414            mSnippetView = new TextView(getContext());
1415            mSnippetView.setSingleLine(true);
1416            mSnippetView.setEllipsize(getTextEllipsis());
1417            mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1418            mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1419            mSnippetView.setActivated(isActivated());
1420            addView(mSnippetView);
1421        }
1422        return mSnippetView;
1423    }
1424
1425    /**
1426     * Returns the text view for the status, creating it if necessary.
1427     */
1428    public TextView getStatusView() {
1429        if (mStatusView == null) {
1430            mStatusView = new TextView(getContext());
1431            mStatusView.setSingleLine(true);
1432            mStatusView.setEllipsize(getTextEllipsis());
1433            mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1434            mStatusView.setTextColor(mSecondaryTextColor);
1435            mStatusView.setActivated(isActivated());
1436            mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1437            addView(mStatusView);
1438        }
1439        return mStatusView;
1440    }
1441
1442    /**
1443     * Adds or updates a text view for the status.
1444     */
1445    public void setStatus(CharSequence text) {
1446        if (TextUtils.isEmpty(text)) {
1447            if (mStatusView != null) {
1448                mStatusView.setVisibility(View.GONE);
1449            }
1450        } else {
1451            getStatusView();
1452            setMarqueeText(mStatusView, text);
1453            mStatusView.setVisibility(VISIBLE);
1454        }
1455    }
1456
1457    /**
1458     * Adds or updates the presence icon view.
1459     */
1460    public void setPresence(Drawable icon) {
1461        if (icon != null) {
1462            if (mPresenceIcon == null) {
1463                mPresenceIcon = new ImageView(getContext());
1464                addView(mPresenceIcon);
1465            }
1466            mPresenceIcon.setImageDrawable(icon);
1467            mPresenceIcon.setScaleType(ScaleType.CENTER);
1468            mPresenceIcon.setVisibility(View.VISIBLE);
1469        } else {
1470            if (mPresenceIcon != null) {
1471                mPresenceIcon.setVisibility(View.GONE);
1472            }
1473        }
1474    }
1475
1476    /**
1477     * Set to display work profile icon or not
1478     *
1479     * @param enabled set to display work profile icon or not
1480     */
1481    public void setWorkProfileIconEnabled(boolean enabled) {
1482        if (mWorkProfileIcon != null) {
1483            mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
1484        } else if (enabled) {
1485            mWorkProfileIcon = new ImageView(getContext());
1486            addView(mWorkProfileIcon);
1487            mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
1488            mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
1489            mWorkProfileIcon.setVisibility(View.VISIBLE);
1490        }
1491    }
1492
1493    private TruncateAt getTextEllipsis() {
1494        return TruncateAt.MARQUEE;
1495    }
1496
1497    public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
1498        CharSequence name = cursor.getString(nameColumnIndex);
1499        setDisplayName(name);
1500
1501        // Since the quick contact content description is derived from the display name and there is
1502        // no guarantee that when the quick contact is initialized the display name is already set,
1503        // do it here too.
1504        if (mQuickContact != null) {
1505            mQuickContact.setContentDescription(getContext().getString(
1506                    R.string.description_quick_contact_for, mNameTextView.getText()));
1507        }
1508    }
1509
1510    public void setDisplayName(CharSequence name, boolean highlight) {
1511        if (!TextUtils.isEmpty(name) && highlight) {
1512            clearHighlightSequences();
1513            addNameHighlightSequence(0, name.length());
1514        }
1515        setDisplayName(name);
1516    }
1517
1518    public void setDisplayName(CharSequence name) {
1519        if (!TextUtils.isEmpty(name)) {
1520            // Chooses the available highlighting method for highlighting.
1521            if (mHighlightedPrefix != null) {
1522                name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
1523            } else if (mNameHighlightSequence.size() != 0) {
1524                final SpannableString spannableName = new SpannableString(name);
1525                for (HighlightSequence highlightSequence : mNameHighlightSequence) {
1526                    mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start,
1527                            highlightSequence.end);
1528                }
1529                name = spannableName;
1530            }
1531        } else {
1532            name = mUnknownNameText;
1533        }
1534        setMarqueeText(getNameTextView(), name);
1535
1536        if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
1537            // Give the text-to-speech engine a hint that it's a phone number
1538            mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
1539            mNameTextView.setContentDescription(
1540                    PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
1541        } else {
1542            // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
1543            // of the name into two separate parts.
1544            mNameTextView.setContentDescription(name.toString());
1545        }
1546    }
1547
1548    public void hideCheckBox() {
1549        if (mCheckBox != null) {
1550            removeView(mCheckBox);
1551            mCheckBox = null;
1552        }
1553    }
1554
1555    public void hideDeleteImageButton() {
1556        if (mDeleteImageButton != null) {
1557            removeView(mDeleteImageButton);
1558            mDeleteImageButton = null;
1559        }
1560    }
1561
1562    public void hideDisplayName() {
1563        if (mNameTextView != null) {
1564            removeView(mNameTextView);
1565            mNameTextView = null;
1566        }
1567    }
1568
1569    public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
1570        cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
1571        int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
1572        if (phoneticNameSize != 0) {
1573            setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
1574        } else {
1575            setPhoneticName(null, 0);
1576        }
1577    }
1578
1579    public void hidePhoneticName() {
1580        if (mPhoneticNameTextView != null) {
1581            removeView(mPhoneticNameTextView);
1582            mPhoneticNameTextView = null;
1583        }
1584    }
1585
1586    /**
1587     * Sets the proper icon (star or presence or nothing) and/or status message.
1588     */
1589    public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
1590            int contactStatusColumnIndex) {
1591        Drawable icon = null;
1592        int presence = 0;
1593        if (!cursor.isNull(presenceColumnIndex)) {
1594            presence = cursor.getInt(presenceColumnIndex);
1595            icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
1596        }
1597        setPresence(icon);
1598
1599        String statusMessage = null;
1600        if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
1601            statusMessage = cursor.getString(contactStatusColumnIndex);
1602        }
1603        // If there is no status message from the contact, but there was a presence value, then use
1604        // the default status message string
1605        if (statusMessage == null && presence != 0) {
1606            statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
1607        }
1608        setStatus(statusMessage);
1609    }
1610
1611    /**
1612     * Shows search snippet for email and phone number matches.
1613     */
1614    public void showSnippet(Cursor cursor, String query, int snippetColumn) {
1615        // TODO: this does not properly handle phone numbers with control characters
1616        // For example if the phone number is 444-5555, the search query 4445 will match the
1617        // number since we normalize it before querying CP2 but the snippet will fail since
1618        // the portion to be highlighted is 444-5 not 4445.
1619        final String snippet = cursor.getString(snippetColumn);
1620        if (snippet == null) {
1621            setSnippet(null);
1622            return;
1623        }
1624        final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0
1625                ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null;
1626        if (snippet.equals(displayName)) {
1627            // If the snippet exactly matches the display name (i.e. the phone number or email
1628            // address is being used as the display name) then no snippet is necessary
1629            setSnippet(null);
1630            return;
1631        }
1632        // Show the snippet with the part of the query that matched it
1633        setSnippet(updateSnippet(snippet, query, displayName));
1634    }
1635
1636    /**
1637     * Shows search snippet.
1638     */
1639    public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
1640        if (cursor.getColumnCount() <= summarySnippetColumnIndex
1641            || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
1642            setSnippet(null);
1643            return;
1644        }
1645
1646        String snippet = cursor.getString(summarySnippetColumnIndex);
1647
1648        // Do client side snippeting if provider didn't do it
1649        final Bundle extras = cursor.getExtras();
1650        if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
1651
1652            final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
1653
1654            String displayName = null;
1655            int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
1656            if (displayNameIndex >= 0) {
1657                displayName = cursor.getString(displayNameIndex);
1658            }
1659
1660            snippet = updateSnippet(snippet, query, displayName);
1661
1662        } else {
1663            if (snippet != null) {
1664                int from = 0;
1665                int to = snippet.length();
1666                int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
1667                if (start == -1) {
1668                    snippet = null;
1669                } else {
1670                    int firstNl = snippet.lastIndexOf('\n', start);
1671                    if (firstNl != -1) {
1672                        from = firstNl + 1;
1673                    }
1674                    int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
1675                    if (end != -1) {
1676                        int lastNl = snippet.indexOf('\n', end);
1677                        if (lastNl != -1) {
1678                            to = lastNl;
1679                        }
1680                    }
1681
1682                    StringBuilder sb = new StringBuilder();
1683                    for (int i = from; i < to; i++) {
1684                        char c = snippet.charAt(i);
1685                        if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
1686                                c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
1687                            sb.append(c);
1688                        }
1689                    }
1690                    snippet = sb.toString();
1691                }
1692            }
1693        }
1694
1695        setSnippet(snippet);
1696    }
1697
1698    /**
1699     * Used for deferred snippets from the database. The contents come back as large strings which
1700     * need to be extracted for display.
1701     *
1702     * @param snippet The snippet from the database.
1703     * @param query The search query substring.
1704     * @param displayName The contact display name.
1705     * @return The proper snippet to display.
1706     */
1707    private String updateSnippet(String snippet, String query, String displayName) {
1708
1709        if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
1710            return null;
1711        }
1712        query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
1713
1714        // If the display name already contains the query term, return empty - snippets should
1715        // not be needed in that case.
1716        if (!TextUtils.isEmpty(displayName)) {
1717            final String lowerDisplayName = displayName.toLowerCase();
1718            final List<String> nameTokens = split(lowerDisplayName);
1719            for (String nameToken : nameTokens) {
1720                if (nameToken.startsWith(query)) {
1721                    return null;
1722                }
1723            }
1724        }
1725
1726        // The snippet may contain multiple data lines.
1727        // Show the first line that matches the query.
1728        final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
1729
1730        if (matched != null && matched.line != null) {
1731            // Tokenize for long strings since the match may be at the end of it.
1732            // Skip this part for short strings since the whole string will be displayed.
1733            // Most contact strings are short so the snippetize method will be called infrequently.
1734            final int lengthThreshold = getResources().getInteger(
1735                    R.integer.snippet_length_before_tokenize);
1736            if (matched.line.length() > lengthThreshold) {
1737                return snippetize(matched.line, matched.startIndex, lengthThreshold);
1738            } else {
1739                return matched.line;
1740            }
1741        }
1742
1743        // No match found.
1744        return null;
1745    }
1746
1747    private String snippetize(String line, int matchIndex, int maxLength) {
1748        // Show up to maxLength characters. But we only show full tokens so show the last full token
1749        // up to maxLength characters. So as many starting tokens as possible before trying ending
1750        // tokens.
1751        int remainingLength = maxLength;
1752        int tempRemainingLength = remainingLength;
1753
1754        // Start the end token after the matched query.
1755        int index = matchIndex;
1756        int endTokenIndex = index;
1757
1758        // Find the match token first.
1759        while (index < line.length()) {
1760            if (!Character.isLetterOrDigit(line.charAt(index))) {
1761                endTokenIndex = index;
1762                remainingLength = tempRemainingLength;
1763                break;
1764            }
1765            tempRemainingLength--;
1766            index++;
1767        }
1768
1769        // Find as much content before the match.
1770        index = matchIndex - 1;
1771        tempRemainingLength = remainingLength;
1772        int startTokenIndex = matchIndex;
1773        while (index > -1 && tempRemainingLength > 0) {
1774            if (!Character.isLetterOrDigit(line.charAt(index))) {
1775                startTokenIndex = index;
1776                remainingLength = tempRemainingLength;
1777            }
1778            tempRemainingLength--;
1779            index--;
1780        }
1781
1782        index = endTokenIndex;
1783        tempRemainingLength = remainingLength;
1784        // Find remaining content at after match.
1785        while (index < line.length() && tempRemainingLength > 0) {
1786            if (!Character.isLetterOrDigit(line.charAt(index))) {
1787                endTokenIndex = index;
1788            }
1789            tempRemainingLength--;
1790            index++;
1791        }
1792        // Append ellipse if there is content before or after.
1793        final StringBuilder sb = new StringBuilder();
1794        if (startTokenIndex > 0) {
1795            sb.append("...");
1796        }
1797        sb.append(line.substring(startTokenIndex, endTokenIndex));
1798        if (endTokenIndex < line.length()) {
1799            sb.append("...");
1800        }
1801        return sb.toString();
1802    }
1803
1804    private static final Pattern SPLIT_PATTERN = Pattern.compile(
1805            "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
1806
1807    /**
1808     * Helper method for splitting a string into tokens.  The lists passed in are populated with
1809     * the
1810     * tokens and offsets into the content of each token.  The tokenization function parses e-mail
1811     * addresses as a single token; otherwise it splits on any non-alphanumeric character.
1812     *
1813     * @param content Content to split.
1814     * @return List of token strings.
1815     */
1816    private static List<String> split(String content) {
1817        final Matcher matcher = SPLIT_PATTERN.matcher(content);
1818        final ArrayList<String> tokens = Lists.newArrayList();
1819        while (matcher.find()) {
1820            tokens.add(matcher.group());
1821        }
1822        return tokens;
1823    }
1824
1825    /**
1826     * Shows data element.
1827     */
1828    public void showData(Cursor cursor, int dataColumnIndex) {
1829        cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
1830        setData(mDataBuffer.data, mDataBuffer.sizeCopied);
1831    }
1832
1833    public void setActivatedStateSupported(boolean flag) {
1834        this.mActivatedStateSupported = flag;
1835    }
1836
1837    public void setAdjustSelectionBoundsEnabled(boolean enabled) {
1838        mAdjustSelectionBoundsEnabled = enabled;
1839    }
1840
1841    @Override
1842    public void requestLayout() {
1843        // We will assume that once measured this will not need to resize
1844        // itself, so there is no need to pass the layout request to the parent
1845        // view (ListView).
1846        forceLayout();
1847    }
1848
1849    public void setPhotoPosition(PhotoPosition photoPosition) {
1850        mPhotoPosition = photoPosition;
1851    }
1852
1853    public PhotoPosition getPhotoPosition() {
1854        return mPhotoPosition;
1855    }
1856
1857    /**
1858     * Set drawable resources directly for the drawable resource of the photo view.
1859     *
1860     * @param drawableId Id of drawable resource.
1861     */
1862    public void setDrawableResource(int drawableId) {
1863        ImageView photo = getPhotoView();
1864        photo.setScaleType(ImageView.ScaleType.CENTER);
1865        final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
1866        final int iconColor =
1867                ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
1868        if (CompatUtils.isLollipopCompatible()) {
1869            photo.setImageDrawable(drawable);
1870            photo.setImageTintList(ColorStateList.valueOf(iconColor));
1871        } else {
1872            final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
1873            DrawableCompat.setTint(drawableWrapper, iconColor);
1874            photo.setImageDrawable(drawableWrapper);
1875        }
1876    }
1877
1878    @Override
1879    public boolean onTouchEvent(MotionEvent event) {
1880        final float x = event.getX();
1881        final float y = event.getY();
1882        // If the touch event's coordinates are not within the view's header, then delegate
1883        // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
1884        // and ignore the touch event.
1885        if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
1886            return super.onTouchEvent(event);
1887        } else {
1888            return true;
1889        }
1890    }
1891
1892    private final boolean pointIsInView(float localX, float localY) {
1893        return localX >= mLeftOffset && localX < mRightOffset
1894                && localY >= 0 && localY < (getBottom() - getTop());
1895    }
1896}
1897