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