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 
17 package com.android.contacts.list;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.TypedArray;
22 import android.database.CharArrayBuffer;
23 import android.database.Cursor;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.Rect;
27 import android.graphics.Typeface;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.provider.ContactsContract;
31 import android.provider.ContactsContract.Contacts;
32 import android.provider.ContactsContract.SearchSnippets;
33 import android.support.v4.content.ContextCompat;
34 import android.support.v4.content.res.ResourcesCompat;
35 import android.support.v4.graphics.drawable.DrawableCompat;
36 import android.support.v7.widget.AppCompatCheckBox;
37 import android.support.v7.widget.AppCompatImageButton;
38 import android.text.Spannable;
39 import android.text.SpannableString;
40 import android.text.TextUtils;
41 import android.text.TextUtils.TruncateAt;
42 import android.util.AttributeSet;
43 import android.util.TypedValue;
44 import android.view.Gravity;
45 import android.view.MotionEvent;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import android.widget.AbsListView.SelectionBoundsAdjuster;
49 import android.widget.ImageView;
50 import android.widget.ImageView.ScaleType;
51 import android.widget.QuickContactBadge;
52 import android.widget.TextView;
53 
54 import com.android.contacts.ContactPresenceIconUtil;
55 import com.android.contacts.ContactStatusUtil;
56 import com.android.contacts.R;
57 import com.android.contacts.compat.CompatUtils;
58 import com.android.contacts.compat.PhoneNumberUtilsCompat;
59 import com.android.contacts.format.TextHighlighter;
60 import com.android.contacts.util.ContactDisplayUtils;
61 import com.android.contacts.util.SearchUtil;
62 import com.android.contacts.util.ViewUtil;
63 
64 import com.google.common.collect.Lists;
65 
66 import java.util.ArrayList;
67 import java.util.List;
68 import java.util.Locale;
69 import java.util.regex.Matcher;
70 import 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 
87 public 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 
HighlightSequence(int start, int end)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 
getDefaultPhotoPosition(boolean opposite)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 
ContactListItemView(Context context)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 
ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon)271     public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) {
272         this(context, attrs);
273 
274         mSupportVideoCallIcon = supportVideoCallIcon;
275     }
276 
ContactListItemView(Context context, AttributeSet attrs)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 
setUnknownNameText(CharSequence unknownNameText)367     public void setUnknownNameText(CharSequence unknownNameText) {
368         mUnknownNameText = unknownNameText;
369     }
370 
setQuickContactEnabled(boolean flag)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      */
setShowVideoCallIcon(boolean showVideoCallIcon, PhoneNumberListAdapter.Listener listener, int position)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      */
setSupportVideoCallIcon(boolean supportVideoCallIcon)424     public void setSupportVideoCallIcon(boolean supportVideoCallIcon) {
425         mSupportVideoCallIcon = supportVideoCallIcon;
426     }
427 
428     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int left, int top, int right, int bottom)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
adjustListItemSelectionBounds(Rect bounds)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 
isVisible(View view)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      */
ensurePhotoViewSize()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 
getDefaultPhotoViewSize()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      */
getDefaultPhotoLayoutParams()942     private LayoutParams getDefaultPhotoLayoutParams() {
943         LayoutParams params = generateDefaultLayoutParams();
944         params.width = getDefaultPhotoViewSize();
945         params.height = params.width;
946         return params;
947     }
948 
949     @Override
drawableStateChanged()950     protected void drawableStateChanged() {
951         super.drawableStateChanged();
952         if (mActivatedStateSupported) {
953             mActivatedBackgroundDrawable.setState(getDrawableState());
954         }
955     }
956 
957     @Override
verifyDrawable(Drawable who)958     protected boolean verifyDrawable(Drawable who) {
959         return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
960     }
961 
962     @Override
jumpDrawablesToCurrentState()963     public void jumpDrawablesToCurrentState() {
964         super.jumpDrawablesToCurrentState();
965         if (mActivatedStateSupported) {
966             mActivatedBackgroundDrawable.jumpToCurrentState();
967         }
968     }
969 
970     @Override
dispatchDraw(Canvas canvas)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      */
setSectionHeader(String title)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 
addTextHeader(String title)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 
updateHeaderText(TextView headerTextView, String title)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 
addStarImageHeader()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 
setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled)1044     public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
1045         mIsSectionHeaderEnabled = isSectionHeaderEnabled;
1046     }
1047 
1048     /**
1049      * Returns the quick contact badge, creating it if necessary.
1050      */
getQuickContact()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      */
getPhotoView()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      */
removePhotoView()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      */
removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)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      */
setHighlightedPrefix(String upperCasePrefix)1122     public void setHighlightedPrefix(String upperCasePrefix) {
1123         mHighlightedPrefix = upperCasePrefix;
1124     }
1125 
1126     /**
1127      * Clears previously set highlight sequences for the view.
1128      */
clearHighlightSequences()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      */
addNameHighlightSequence(int start, int end)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      */
addNumberHighlightSequence(int start, int end)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      */
getNameTextView()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      */
setPhoneticName(char[] text, int size)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      */
getPhoneticNameTextView()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      */
setLabel(CharSequence text)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      */
getLabelView()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      */
setData(char[] text, int size)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      */
setPhoneNumber(String text, String countryIso)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 
setMarqueeText(TextView textView, char[] text, int size)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 
setMarqueeText(TextView textView, CharSequence text)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      */
getCheckBox()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      */
getDeleteImageButton( final MultiSelectEntryContactListAdapter.DeleteContactListener listener, final int position)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      */
getDataView()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      */
setSnippet(String text)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      */
getSnippetView()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      */
getStatusView()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      */
setStatus(CharSequence text)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      */
setPresence(Drawable icon)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      */
setWorkProfileIconEnabled(boolean enabled)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 
getTextEllipsis()1493     private TruncateAt getTextEllipsis() {
1494         return TruncateAt.MARQUEE;
1495     }
1496 
showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)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 
setDisplayName(CharSequence name, boolean highlight)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 
setDisplayName(CharSequence name)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 
hideCheckBox()1548     public void hideCheckBox() {
1549         if (mCheckBox != null) {
1550             removeView(mCheckBox);
1551             mCheckBox = null;
1552         }
1553     }
1554 
hideDeleteImageButton()1555     public void hideDeleteImageButton() {
1556         if (mDeleteImageButton != null) {
1557             removeView(mDeleteImageButton);
1558             mDeleteImageButton = null;
1559         }
1560     }
1561 
hideDisplayName()1562     public void hideDisplayName() {
1563         if (mNameTextView != null) {
1564             removeView(mNameTextView);
1565             mNameTextView = null;
1566         }
1567     }
1568 
showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)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 
hidePhoneticName()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      */
showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)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      */
showSnippet(Cursor cursor, String query, int snippetColumn)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      */
showSnippet(Cursor cursor, int summarySnippetColumnIndex)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      */
updateSnippet(String snippet, String query, String displayName)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 
snippetize(String line, int matchIndex, int maxLength)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      */
split(String content)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      */
showData(Cursor cursor, int dataColumnIndex)1828     public void showData(Cursor cursor, int dataColumnIndex) {
1829         cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
1830         setData(mDataBuffer.data, mDataBuffer.sizeCopied);
1831     }
1832 
setActivatedStateSupported(boolean flag)1833     public void setActivatedStateSupported(boolean flag) {
1834         this.mActivatedStateSupported = flag;
1835     }
1836 
setAdjustSelectionBoundsEnabled(boolean enabled)1837     public void setAdjustSelectionBoundsEnabled(boolean enabled) {
1838         mAdjustSelectionBoundsEnabled = enabled;
1839     }
1840 
1841     @Override
requestLayout()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 
setPhotoPosition(PhotoPosition photoPosition)1849     public void setPhotoPosition(PhotoPosition photoPosition) {
1850         mPhotoPosition = photoPosition;
1851     }
1852 
getPhotoPosition()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      */
setDrawableResource(int drawableId)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
onTouchEvent(MotionEvent event)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 
pointIsInView(float localX, float localY)1892     private final boolean pointIsInView(float localX, float localY) {
1893         return localX >= mLeftOffset && localX < mRightOffset
1894                 && localY >= 0 && localY < (getBottom() - getTop());
1895     }
1896 }
1897