• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                 + getPaddingBottom() + getPaddingTop());
593 
594         // Make sure the height is at least as high as the photo
595         height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
596 
597         // Make sure height is at least the preferred height
598         height = Math.max(height, preferredHeight);
599 
600         // Measure the header if it is visible.
601         if (mHeaderView != null && mHeaderView.getVisibility() == VISIBLE) {
602             mHeaderView.measure(
603                     MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
604                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
605         }
606 
607         setMeasuredDimension(specWidth, height);
608     }
609 
610     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)611     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
612         final int height = bottom - top;
613         final int width = right - left;
614 
615         // Determine the vertical bounds by laying out the header first.
616         int topBound = 0;
617         int bottomBound = height;
618         int leftBound = getPaddingLeft();
619         int rightBound = width - getPaddingRight();
620 
621         final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);
622 
623         // Put the section header on the left side of the contact view.
624         if (mIsSectionHeaderEnabled) {
625             if (mHeaderView != null) {
626                 int headerHeight = mHeaderView.getMeasuredHeight();
627                 int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop;
628 
629                 mHeaderView.layout(
630                         isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
631                         headerTopBound,
632                         isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
633                         headerTopBound + headerHeight);
634             }
635             if (isLayoutRtl) {
636                 rightBound -= mHeaderWidth;
637             } else {
638                 leftBound += mHeaderWidth;
639             }
640         }
641 
642         mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound);
643         mLeftOffset = left + leftBound;
644         mRightOffset = left + rightBound;
645         if (isLayoutRtl) {
646             rightBound -= mGapBetweenIndexerAndImage;
647         } else {
648             leftBound += mGapBetweenIndexerAndImage;
649         }
650 
651         if (mActivatedStateSupported && isActivated()) {
652             mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
653         }
654 
655         if (isVisible(mCheckBox)) {
656             final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2;
657             if (mPhotoPosition == PhotoPosition.LEFT) {
658                 mCheckBox.layout(rightBound - mGapFromScrollBar - mCheckBoxWidth,
659                         photoTop,
660                         rightBound - mGapFromScrollBar,
661                         photoTop + mCheckBoxHeight);
662             } else {
663                 mCheckBox.layout(leftBound + mGapFromScrollBar,
664                         photoTop,
665                         leftBound + mGapFromScrollBar + mCheckBoxWidth,
666                         photoTop + mCheckBoxHeight);
667             }
668         }
669 
670         if (isVisible(mDeleteImageButton)) {
671             final int photoTop = topBound + (bottomBound - topBound - mDeleteImageButtonHeight) / 2;
672             final int mDeleteImageButtonSize = mDeleteImageButtonHeight > mDeleteImageButtonWidth
673                     ? mDeleteImageButtonHeight : mDeleteImageButtonWidth;
674             if (mPhotoPosition == PhotoPosition.LEFT) {
675                 mDeleteImageButton.layout(rightBound - mDeleteImageButtonSize,
676                         photoTop,
677                         rightBound,
678                         photoTop + mDeleteImageButtonSize);
679                 rightBound -= mDeleteImageButtonSize;
680             } else {
681                 mDeleteImageButton.layout(leftBound,
682                         photoTop,
683                         leftBound + mDeleteImageButtonSize,
684                         photoTop + mDeleteImageButtonSize);
685                 leftBound += mDeleteImageButtonSize;
686             }
687         }
688 
689         final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
690         if (mPhotoPosition == PhotoPosition.LEFT) {
691             // Photo is the left most view. All the other Views should on the right of the photo.
692             if (photoView != null) {
693                 // Center the photo vertically
694                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2
695                         + mAvatarOffsetTop;
696                 photoView.layout(
697                         leftBound,
698                         photoTop,
699                         leftBound + mPhotoViewWidth,
700                         photoTop + mPhotoViewHeight);
701                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
702             } else if (mKeepHorizontalPaddingForPhotoView) {
703                 // Draw nothing but keep the padding.
704                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
705             }
706         } else {
707             // Photo is the right most view. Right bound should be adjusted that way.
708             if (photoView != null) {
709                 // Center the photo vertically
710                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2
711                         + mAvatarOffsetTop;
712                 photoView.layout(
713                         rightBound - mPhotoViewWidth,
714                         photoTop,
715                         rightBound,
716                         photoTop + mPhotoViewHeight);
717                 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
718             } else if (mKeepHorizontalPaddingForPhotoView) {
719                 // Draw nothing but keep the padding.
720                 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
721             }
722 
723             // Add indent between left-most padding and texts.
724             leftBound += mTextIndent;
725         }
726 
727         if (mSupportVideoCallIcon) {
728             // Place the video call button at the end of the list (e.g. take into account RTL mode).
729             if (isVisible(mVideoCallIcon)) {
730                 // Center the video icon vertically
731                 final int videoIconTop = topBound +
732                         (bottomBound - topBound - mVideoCallIconSize) / 2;
733 
734                 if (!isLayoutRtl) {
735                     // When photo is on left, video icon is placed on the right edge.
736                     mVideoCallIcon.layout(rightBound - mVideoCallIconSize,
737                             videoIconTop,
738                             rightBound,
739                             videoIconTop + mVideoCallIconSize);
740                 } else {
741                     // When photo is on right, video icon is placed on the left edge.
742                     mVideoCallIcon.layout(leftBound,
743                             videoIconTop,
744                             leftBound + mVideoCallIconSize,
745                             videoIconTop + mVideoCallIconSize);
746                 }
747             }
748 
749             if (mPhotoPosition == PhotoPosition.LEFT) {
750                 rightBound -= (mVideoCallIconSize + mVideoCallIconMargin);
751             } else {
752                 leftBound += mVideoCallIconSize + mVideoCallIconMargin;
753             }
754         }
755 
756 
757         // Center text vertically, then apply the top offset.
758         final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
759                 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
760         int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop;
761 
762         // Work Profile icon align top
763         int workProfileIconWidth = 0;
764         if (isVisible(mWorkProfileIcon)) {
765             workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
766             final int distanceFromEnd = mCheckBoxWidth > 0
767                     ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
768             if (mPhotoPosition == PhotoPosition.LEFT) {
769                 // When photo is on left, label is placed on the right edge of the list item.
770                 mWorkProfileIcon.layout(rightBound - workProfileIconWidth - distanceFromEnd,
771                         textTopBound,
772                         rightBound - distanceFromEnd,
773                         textTopBound + mNameTextViewHeight);
774             } else {
775                 // When photo is on right, label is placed on the left of data view.
776                 mWorkProfileIcon.layout(leftBound + distanceFromEnd,
777                         textTopBound,
778                         leftBound + workProfileIconWidth + distanceFromEnd,
779                         textTopBound + mNameTextViewHeight);
780             }
781         }
782 
783         // Layout all text view and presence icon
784         // Put name TextView first
785         if (isVisible(mNameTextView)) {
786             final int distanceFromEnd = workProfileIconWidth
787                     + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
788             if (mPhotoPosition == PhotoPosition.LEFT) {
789                 mNameTextView.layout(leftBound,
790                         textTopBound,
791                         rightBound - distanceFromEnd,
792                         textTopBound + mNameTextViewHeight);
793             } else {
794                 mNameTextView.layout(leftBound + distanceFromEnd,
795                         textTopBound,
796                         rightBound,
797                         textTopBound + mNameTextViewHeight);
798             }
799         }
800 
801         if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
802             textTopBound += mNameTextViewHeight;
803         }
804 
805         // Presence and status
806         if (isLayoutRtl) {
807             int statusRightBound = rightBound;
808             if (isVisible(mPresenceIcon)) {
809                 int iconWidth = mPresenceIcon.getMeasuredWidth();
810                 mPresenceIcon.layout(
811                         rightBound - iconWidth,
812                         textTopBound,
813                         rightBound,
814                         textTopBound + mStatusTextViewHeight);
815                 statusRightBound -= (iconWidth + mPresenceIconMargin);
816             }
817 
818             if (isVisible(mStatusView)) {
819                 mStatusView.layout(leftBound,
820                         textTopBound,
821                         statusRightBound,
822                         textTopBound + mStatusTextViewHeight);
823             }
824         } else {
825             int statusLeftBound = leftBound;
826             if (isVisible(mPresenceIcon)) {
827                 int iconWidth = mPresenceIcon.getMeasuredWidth();
828                 mPresenceIcon.layout(
829                         leftBound,
830                         textTopBound,
831                         leftBound + iconWidth,
832                         textTopBound + mStatusTextViewHeight);
833                 statusLeftBound += (iconWidth + mPresenceIconMargin);
834             }
835 
836             if (isVisible(mStatusView)) {
837                 mStatusView.layout(statusLeftBound,
838                         textTopBound,
839                         rightBound,
840                         textTopBound + mStatusTextViewHeight);
841             }
842         }
843 
844         if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
845             textTopBound += mStatusTextViewHeight;
846         }
847 
848         // Rest of text views
849         int dataLeftBound = leftBound;
850         if (isVisible(mPhoneticNameTextView)) {
851             mPhoneticNameTextView.layout(leftBound,
852                     textTopBound,
853                     rightBound,
854                     textTopBound + mPhoneticNameTextViewHeight);
855             textTopBound += mPhoneticNameTextViewHeight;
856         }
857 
858         // Label and Data align bottom.
859         if (isVisible(mLabelView)) {
860             if (!isLayoutRtl) {
861                 mLabelView.layout(dataLeftBound,
862                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
863                         rightBound,
864                         textTopBound + mLabelAndDataViewMaxHeight);
865                 dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
866             } else {
867                 dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
868                 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
869                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
870                         rightBound,
871                         textTopBound + mLabelAndDataViewMaxHeight);
872                 rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
873             }
874         }
875 
876         if (isVisible(mDataView)) {
877             if (!isLayoutRtl) {
878                 mDataView.layout(dataLeftBound,
879                         textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
880                         rightBound,
881                         textTopBound + mLabelAndDataViewMaxHeight);
882             } else {
883                 mDataView.layout(rightBound - mDataView.getMeasuredWidth(),
884                         textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
885                         rightBound,
886                         textTopBound + mLabelAndDataViewMaxHeight);
887             }
888         }
889         if (isVisible(mLabelView) || isVisible(mDataView)) {
890             textTopBound += mLabelAndDataViewMaxHeight;
891         }
892 
893         if (isVisible(mSnippetView)) {
894             mSnippetView.layout(leftBound,
895                     textTopBound,
896                     rightBound,
897                     textTopBound + mSnippetTextViewHeight);
898         }
899     }
900 
901     @Override
adjustListItemSelectionBounds(Rect bounds)902     public void adjustListItemSelectionBounds(Rect bounds) {
903         if (mAdjustSelectionBoundsEnabled) {
904             bounds.top += mBoundsWithoutHeader.top;
905             bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
906             bounds.left = mBoundsWithoutHeader.left;
907             bounds.right = mBoundsWithoutHeader.right;
908         }
909     }
910 
isVisible(View view)911     protected boolean isVisible(View view) {
912         return view != null && view.getVisibility() == View.VISIBLE;
913     }
914 
915     /**
916      * Extracts width and height from the style
917      */
ensurePhotoViewSize()918     private void ensurePhotoViewSize() {
919         if (!mPhotoViewWidthAndHeightAreReady) {
920             mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
921             if (!mQuickContactEnabled && mPhotoView == null) {
922                 if (!mKeepHorizontalPaddingForPhotoView) {
923                     mPhotoViewWidth = 0;
924                 }
925                 if (!mKeepVerticalPaddingForPhotoView) {
926                     mPhotoViewHeight = 0;
927                 }
928             }
929 
930             mPhotoViewWidthAndHeightAreReady = true;
931         }
932     }
933 
getDefaultPhotoViewSize()934     protected int getDefaultPhotoViewSize() {
935         return mDefaultPhotoViewSize;
936     }
937 
938     /**
939      * Gets a LayoutParam that corresponds to the default photo size.
940      *
941      * @return A new LayoutParam.
942      */
getDefaultPhotoLayoutParams()943     private LayoutParams getDefaultPhotoLayoutParams() {
944         LayoutParams params = generateDefaultLayoutParams();
945         params.width = getDefaultPhotoViewSize();
946         params.height = params.width;
947         return params;
948     }
949 
950     @Override
drawableStateChanged()951     protected void drawableStateChanged() {
952         super.drawableStateChanged();
953         if (mActivatedStateSupported) {
954             mActivatedBackgroundDrawable.setState(getDrawableState());
955         }
956     }
957 
958     @Override
verifyDrawable(Drawable who)959     protected boolean verifyDrawable(Drawable who) {
960         return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
961     }
962 
963     @Override
jumpDrawablesToCurrentState()964     public void jumpDrawablesToCurrentState() {
965         super.jumpDrawablesToCurrentState();
966         if (mActivatedStateSupported) {
967             mActivatedBackgroundDrawable.jumpToCurrentState();
968         }
969     }
970 
971     @Override
dispatchDraw(Canvas canvas)972     public void dispatchDraw(Canvas canvas) {
973         if (mActivatedStateSupported && isActivated()) {
974             mActivatedBackgroundDrawable.draw(canvas);
975         }
976 
977         super.dispatchDraw(canvas);
978     }
979 
980     /**
981      * Sets section header or makes it invisible if the title is null.
982      */
setSectionHeader(String title)983     public void setSectionHeader(String title) {
984         if (title != null) {
985             // Empty section title is the favorites so show the star here.
986             if (title.isEmpty()) {
987                 if (mHeaderView == null) {
988                     addStarImageHeader();
989                 } else if (mHeaderView instanceof TextView) {
990                     removeView(mHeaderView);
991                     addStarImageHeader();
992                 } else {
993                     mHeaderView.setVisibility(View.VISIBLE);
994                 }
995             } else {
996                 if (mHeaderView == null) {
997                     addTextHeader(title);
998                 } else if (mHeaderView instanceof ImageView) {
999                     removeView(mHeaderView);
1000                     addTextHeader(title);
1001                 } else {
1002                     updateHeaderText((TextView) mHeaderView, title);
1003                 }
1004             }
1005         } else if (mHeaderView != null) {
1006             mHeaderView.setVisibility(View.GONE);
1007         }
1008     }
1009 
addTextHeader(String title)1010     private void addTextHeader(String title) {
1011         mHeaderView = new TextView(getContext());
1012         final TextView headerTextView = (TextView) mHeaderView;
1013         headerTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle);
1014         headerTextView.setGravity(Gravity.CENTER_HORIZONTAL);
1015         updateHeaderText(headerTextView, title);
1016         addView(headerTextView);
1017     }
1018 
updateHeaderText(TextView headerTextView, String title)1019     private void updateHeaderText(TextView headerTextView, String title) {
1020         setMarqueeText(headerTextView, title);
1021         headerTextView.setAllCaps(true);
1022         if (ContactsSectionIndexer.BLANK_HEADER_STRING.equals(title)) {
1023             headerTextView.setContentDescription(
1024                     getContext().getString(R.string.description_no_name_header));
1025         } else {
1026             headerTextView.setContentDescription(title);
1027         }
1028         headerTextView.setVisibility(View.VISIBLE);
1029     }
1030 
addStarImageHeader()1031     private void addStarImageHeader() {
1032         mHeaderView = new ImageView(getContext());
1033         final ImageView headerImageView = (ImageView) mHeaderView;
1034         headerImageView.setImageDrawable(
1035                 getResources().getDrawable(R.drawable.quantum_ic_star_vd_theme_24,
1036                         getContext().getTheme()));
1037         headerImageView.setImageTintList(ColorStateList.valueOf(getResources()
1038                 .getColor(R.color.material_star_pink)));
1039         headerImageView.setContentDescription(
1040                 getContext().getString(R.string.contactsFavoritesLabel));
1041         headerImageView.setVisibility(View.VISIBLE);
1042         addView(headerImageView);
1043     }
1044 
setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled)1045     public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
1046         mIsSectionHeaderEnabled = isSectionHeaderEnabled;
1047     }
1048 
1049     /**
1050      * Returns the quick contact badge, creating it if necessary.
1051      */
getQuickContact()1052     public QuickContactBadge getQuickContact() {
1053         if (!mQuickContactEnabled) {
1054             throw new IllegalStateException("QuickContact is disabled for this view");
1055         }
1056         if (mQuickContact == null) {
1057             mQuickContact = new QuickContactBadge(getContext());
1058             if (CompatUtils.isLollipopCompatible()) {
1059                 mQuickContact.setOverlay(null);
1060             }
1061             mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
1062             if (mNameTextView != null) {
1063                 mQuickContact.setContentDescription(getContext().getString(
1064                         R.string.description_quick_contact_for, mNameTextView.getText()));
1065             }
1066 
1067             addView(mQuickContact);
1068             mPhotoViewWidthAndHeightAreReady = false;
1069         }
1070         return mQuickContact;
1071     }
1072 
1073     /**
1074      * Returns the photo view, creating it if necessary.
1075      */
getPhotoView()1076     public ImageView getPhotoView() {
1077         if (mPhotoView == null) {
1078             mPhotoView = new ImageView(getContext());
1079             mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
1080             // Quick contact style used above will set a background - remove it
1081             mPhotoView.setBackground(null);
1082             addView(mPhotoView);
1083             mPhotoViewWidthAndHeightAreReady = false;
1084         }
1085         return mPhotoView;
1086     }
1087 
1088     /**
1089      * Removes the photo view.
1090      */
removePhotoView()1091     public void removePhotoView() {
1092         removePhotoView(false, true);
1093     }
1094 
1095     /**
1096      * Removes the photo view.
1097      *
1098      * @param keepHorizontalPadding True means data on the right side will have
1099      *            padding on left, pretending there is still a photo view.
1100      * @param keepVerticalPadding True means the View will have some height
1101      *            enough for accommodating a photo view.
1102      */
removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)1103     public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
1104         mPhotoViewWidthAndHeightAreReady = false;
1105         mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
1106         mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
1107         if (mPhotoView != null) {
1108             removeView(mPhotoView);
1109             mPhotoView = null;
1110         }
1111         if (mQuickContact != null) {
1112             removeView(mQuickContact);
1113             mQuickContact = null;
1114         }
1115     }
1116 
1117     /**
1118      * Sets a word prefix that will be highlighted if encountered in fields like
1119      * name and search snippet. This will disable the mask highlighting for names.
1120      * <p>
1121      * NOTE: must be all upper-case
1122      */
setHighlightedPrefix(String upperCasePrefix)1123     public void setHighlightedPrefix(String upperCasePrefix) {
1124         mHighlightedPrefix = upperCasePrefix;
1125     }
1126 
1127     /**
1128      * Clears previously set highlight sequences for the view.
1129      */
clearHighlightSequences()1130     public void clearHighlightSequences() {
1131         mNameHighlightSequence.clear();
1132         mNumberHighlightSequence.clear();
1133         mHighlightedPrefix = null;
1134     }
1135 
1136     /**
1137      * Adds a highlight sequence to the name highlighter.
1138      * @param start The start position of the highlight sequence.
1139      * @param end The end position of the highlight sequence.
1140      */
addNameHighlightSequence(int start, int end)1141     public void addNameHighlightSequence(int start, int end) {
1142         mNameHighlightSequence.add(new HighlightSequence(start, end));
1143     }
1144 
1145     /**
1146      * Adds a highlight sequence to the number highlighter.
1147      * @param start The start position of the highlight sequence.
1148      * @param end The end position of the highlight sequence.
1149      */
addNumberHighlightSequence(int start, int end)1150     public void addNumberHighlightSequence(int start, int end) {
1151         mNumberHighlightSequence.add(new HighlightSequence(start, end));
1152     }
1153 
1154     /**
1155      * Returns the text view for the contact name, creating it if necessary.
1156      */
getNameTextView()1157     public TextView getNameTextView() {
1158         if (mNameTextView == null) {
1159             mNameTextView = new TextView(getContext());
1160             mNameTextView.setSingleLine(true);
1161             mNameTextView.setEllipsize(getTextEllipsis());
1162             mNameTextView.setTextColor(ResourcesCompat.getColorStateList(getResources(),
1163                     R.color.contact_list_name_text_color, getContext().getTheme()));
1164             mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
1165             // Manually call setActivated() since this view may be added after the first
1166             // setActivated() call toward this whole item view.
1167             mNameTextView.setActivated(isActivated());
1168             mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
1169             mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1170             mNameTextView.setId(R.id.cliv_name_textview);
1171             if (CompatUtils.isLollipopCompatible()) {
1172                 mNameTextView.setElegantTextHeight(false);
1173             }
1174             addView(mNameTextView);
1175         }
1176         return mNameTextView;
1177     }
1178 
1179     /**
1180      * Adds or updates a text view for the phonetic name.
1181      */
setPhoneticName(char[] text, int size)1182     public void setPhoneticName(char[] text, int size) {
1183         if (text == null || size == 0) {
1184             if (mPhoneticNameTextView != null) {
1185                 mPhoneticNameTextView.setVisibility(View.GONE);
1186             }
1187         } else {
1188             getPhoneticNameTextView();
1189             setMarqueeText(mPhoneticNameTextView, text, size);
1190             mPhoneticNameTextView.setVisibility(VISIBLE);
1191         }
1192     }
1193 
1194     /**
1195      * Returns the text view for the phonetic name, creating it if necessary.
1196      */
getPhoneticNameTextView()1197     public TextView getPhoneticNameTextView() {
1198         if (mPhoneticNameTextView == null) {
1199             mPhoneticNameTextView = new TextView(getContext());
1200             mPhoneticNameTextView.setSingleLine(true);
1201             mPhoneticNameTextView.setEllipsize(getTextEllipsis());
1202             mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1203             mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1204             mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
1205             mPhoneticNameTextView.setActivated(isActivated());
1206             mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview);
1207             addView(mPhoneticNameTextView);
1208         }
1209         return mPhoneticNameTextView;
1210     }
1211 
1212     /**
1213      * Adds or updates a text view for the data label.
1214      */
setLabel(CharSequence text)1215     public void setLabel(CharSequence text) {
1216         if (TextUtils.isEmpty(text)) {
1217             if (mLabelView != null) {
1218                 mLabelView.setVisibility(View.GONE);
1219             }
1220         } else {
1221             getLabelView();
1222             setMarqueeText(mLabelView, text);
1223             mLabelView.setVisibility(VISIBLE);
1224         }
1225     }
1226 
1227     /**
1228      * Returns the text view for the data label, creating it if necessary.
1229      */
getLabelView()1230     public TextView getLabelView() {
1231         if (mLabelView == null) {
1232             mLabelView = new TextView(getContext());
1233             mLabelView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
1234                     LayoutParams.WRAP_CONTENT));
1235 
1236             mLabelView.setSingleLine(true);
1237             mLabelView.setEllipsize(getTextEllipsis());
1238             mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
1239             if (mPhotoPosition == PhotoPosition.LEFT) {
1240                 mLabelView.setAllCaps(true);
1241             } else {
1242                 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
1243             }
1244             mLabelView.setActivated(isActivated());
1245             mLabelView.setId(R.id.cliv_label_textview);
1246             addView(mLabelView);
1247         }
1248         return mLabelView;
1249     }
1250 
1251     /**
1252      * Adds or updates a text view for the data element.
1253      */
setData(char[] text, int size)1254     public void setData(char[] text, int size) {
1255         if (text == null || size == 0) {
1256             if (mDataView != null) {
1257                 mDataView.setVisibility(View.GONE);
1258             }
1259         } else {
1260             getDataView();
1261             setMarqueeText(mDataView, text, size);
1262             mDataView.setVisibility(VISIBLE);
1263         }
1264     }
1265 
1266     /**
1267      * Sets phone number for a list item. This takes care of number highlighting if the highlight
1268      * mask exists.
1269      */
setPhoneNumber(String text, String countryIso)1270     public void setPhoneNumber(String text, String countryIso) {
1271         if (text == null) {
1272             if (mDataView != null) {
1273                 mDataView.setVisibility(View.GONE);
1274             }
1275         } else {
1276             getDataView();
1277 
1278             // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
1279             // mDataView. Make sure that determination of the highlight sequences are done only
1280             // after number formatting.
1281 
1282             // Sets phone number texts for display after highlighting it, if applicable.
1283             // CharSequence textToSet = text;
1284             final SpannableString textToSet = new SpannableString(text);
1285 
1286             if (mNumberHighlightSequence.size() != 0) {
1287                 final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
1288                 mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start,
1289                         highlightSequence.end);
1290             }
1291 
1292             setMarqueeText(mDataView, textToSet);
1293             mDataView.setVisibility(VISIBLE);
1294 
1295             // We have a phone number as "mDataView" so make it always LTR and VIEW_START
1296             mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
1297             mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1298         }
1299     }
1300 
setMarqueeText(TextView textView, char[] text, int size)1301     private void setMarqueeText(TextView textView, char[] text, int size) {
1302         if (getTextEllipsis() == TruncateAt.MARQUEE) {
1303             setMarqueeText(textView, new String(text, 0, size));
1304         } else {
1305             textView.setText(text, 0, size);
1306         }
1307     }
1308 
setMarqueeText(TextView textView, CharSequence text)1309     private void setMarqueeText(TextView textView, CharSequence text) {
1310         if (getTextEllipsis() == TruncateAt.MARQUEE) {
1311             // To show MARQUEE correctly (with END effect during non-active state), we need
1312             // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
1313             final SpannableString spannable = new SpannableString(text);
1314             spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
1315                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1316             textView.setText(spannable);
1317         } else {
1318             textView.setText(text);
1319         }
1320     }
1321 
1322     /**
1323      * Returns the {@link AppCompatCheckBox} view, creating it if necessary.
1324      */
getCheckBox()1325     public AppCompatCheckBox getCheckBox() {
1326         if (mCheckBox == null) {
1327             mCheckBox = new AppCompatCheckBox(getContext());
1328             // Make non-focusable, so the rest of the ContactListItemView can be clicked.
1329             mCheckBox.setFocusable(false);
1330             addView(mCheckBox);
1331         }
1332         return mCheckBox;
1333     }
1334 
1335     /**
1336      * Returns the {@link AppCompatImageButton} delete button, creating it if necessary.
1337      */
getDeleteImageButton( final MultiSelectEntryContactListAdapter.DeleteContactListener listener, final int position)1338     public AppCompatImageButton getDeleteImageButton(
1339             final MultiSelectEntryContactListAdapter.DeleteContactListener listener,
1340             final int position) {
1341         if (mDeleteImageButton == null) {
1342             mDeleteImageButton = new AppCompatImageButton(getContext());
1343             mDeleteImageButton.setImageResource(R.drawable.quantum_ic_cancel_vd_theme_24);
1344             mDeleteImageButton.setScaleType(ScaleType.CENTER);
1345             mDeleteImageButton.setBackgroundColor(Color.TRANSPARENT);
1346             mDeleteImageButton.setContentDescription(
1347                     getResources().getString(R.string.description_delete_contact));
1348             if (CompatUtils. isLollipopCompatible()) {
1349                 final TypedValue typedValue = new TypedValue();
1350                 getContext().getTheme().resolveAttribute(
1351                         android.R.attr.selectableItemBackgroundBorderless, typedValue, true);
1352                 mDeleteImageButton.setBackgroundResource(typedValue.resourceId);
1353             }
1354             addView(mDeleteImageButton);
1355         }
1356         // Reset onClickListener because after reloading the view, position might be changed.
1357         mDeleteImageButton.setOnClickListener(new OnClickListener() {
1358             @Override
1359             public void onClick(View v) {
1360                 // Inform the adapter that delete icon was clicked.
1361                 if (listener != null) {
1362                     listener.onContactDeleteClicked(position);
1363                 }
1364             }
1365         });
1366         return mDeleteImageButton;
1367     }
1368 
1369     /**
1370      * Returns the text view for the data text, creating it if necessary.
1371      */
getDataView()1372     public TextView getDataView() {
1373         if (mDataView == null) {
1374             mDataView = new TextView(getContext());
1375             mDataView.setSingleLine(true);
1376             mDataView.setEllipsize(getTextEllipsis());
1377             mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
1378             mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1379             mDataView.setActivated(isActivated());
1380             mDataView.setId(R.id.cliv_data_view);
1381             if (CompatUtils.isLollipopCompatible()) {
1382                 mDataView.setElegantTextHeight(false);
1383             }
1384             addView(mDataView);
1385         }
1386         return mDataView;
1387     }
1388 
1389     /**
1390      * Adds or updates a text view for the search snippet.
1391      */
setSnippet(String text)1392     public void setSnippet(String text) {
1393         if (TextUtils.isEmpty(text)) {
1394             if (mSnippetView != null) {
1395                 mSnippetView.setVisibility(View.GONE);
1396             }
1397         } else {
1398             mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
1399             mSnippetView.setVisibility(VISIBLE);
1400             if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
1401                 // Give the text-to-speech engine a hint that it's a phone number
1402                 mSnippetView.setContentDescription(
1403                         PhoneNumberUtilsCompat.createTtsSpannable(text));
1404             } else {
1405                 mSnippetView.setContentDescription(null);
1406             }
1407         }
1408     }
1409 
1410     /**
1411      * Returns the text view for the search snippet, creating it if necessary.
1412      */
getSnippetView()1413     public TextView getSnippetView() {
1414         if (mSnippetView == null) {
1415             mSnippetView = new TextView(getContext());
1416             mSnippetView.setSingleLine(true);
1417             mSnippetView.setEllipsize(getTextEllipsis());
1418             mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1419             mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1420             mSnippetView.setActivated(isActivated());
1421             addView(mSnippetView);
1422         }
1423         return mSnippetView;
1424     }
1425 
1426     /**
1427      * Returns the text view for the status, creating it if necessary.
1428      */
getStatusView()1429     public TextView getStatusView() {
1430         if (mStatusView == null) {
1431             mStatusView = new TextView(getContext());
1432             mStatusView.setSingleLine(true);
1433             mStatusView.setEllipsize(getTextEllipsis());
1434             mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
1435             mStatusView.setTextColor(mSecondaryTextColor);
1436             mStatusView.setActivated(isActivated());
1437             mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1438             addView(mStatusView);
1439         }
1440         return mStatusView;
1441     }
1442 
1443     /**
1444      * Adds or updates a text view for the status.
1445      */
setStatus(CharSequence text)1446     public void setStatus(CharSequence text) {
1447         if (TextUtils.isEmpty(text)) {
1448             if (mStatusView != null) {
1449                 mStatusView.setVisibility(View.GONE);
1450             }
1451         } else {
1452             getStatusView();
1453             setMarqueeText(mStatusView, text);
1454             mStatusView.setVisibility(VISIBLE);
1455         }
1456     }
1457 
1458     /**
1459      * Adds or updates the presence icon view.
1460      */
setPresence(Drawable icon)1461     public void setPresence(Drawable icon) {
1462         if (icon != null) {
1463             if (mPresenceIcon == null) {
1464                 mPresenceIcon = new ImageView(getContext());
1465                 addView(mPresenceIcon);
1466             }
1467             mPresenceIcon.setImageDrawable(icon);
1468             mPresenceIcon.setScaleType(ScaleType.CENTER);
1469             mPresenceIcon.setVisibility(View.VISIBLE);
1470         } else {
1471             if (mPresenceIcon != null) {
1472                 mPresenceIcon.setVisibility(View.GONE);
1473             }
1474         }
1475     }
1476 
1477     /**
1478      * Set to display work profile icon or not
1479      *
1480      * @param enabled set to display work profile icon or not
1481      */
setWorkProfileIconEnabled(boolean enabled)1482     public void setWorkProfileIconEnabled(boolean enabled) {
1483         if (mWorkProfileIcon != null) {
1484             mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
1485         } else if (enabled) {
1486             mWorkProfileIcon = new ImageView(getContext());
1487             addView(mWorkProfileIcon);
1488             mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
1489             mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
1490             mWorkProfileIcon.setVisibility(View.VISIBLE);
1491         }
1492     }
1493 
getTextEllipsis()1494     private TruncateAt getTextEllipsis() {
1495         return TruncateAt.MARQUEE;
1496     }
1497 
showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)1498     public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
1499         CharSequence name = cursor.getString(nameColumnIndex);
1500         setDisplayName(name);
1501 
1502         // Since the quick contact content description is derived from the display name and there is
1503         // no guarantee that when the quick contact is initialized the display name is already set,
1504         // do it here too.
1505         if (mQuickContact != null) {
1506             mQuickContact.setContentDescription(getContext().getString(
1507                     R.string.description_quick_contact_for, mNameTextView.getText()));
1508         }
1509     }
1510 
setDisplayName(CharSequence name, boolean highlight)1511     public void setDisplayName(CharSequence name, boolean highlight) {
1512         if (!TextUtils.isEmpty(name) && highlight) {
1513             clearHighlightSequences();
1514             addNameHighlightSequence(0, name.length());
1515         }
1516         setDisplayName(name);
1517     }
1518 
setDisplayName(CharSequence name)1519     public void setDisplayName(CharSequence name) {
1520         if (!TextUtils.isEmpty(name)) {
1521             // Chooses the available highlighting method for highlighting.
1522             if (mHighlightedPrefix != null) {
1523                 name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
1524             } else if (mNameHighlightSequence.size() != 0) {
1525                 final SpannableString spannableName = new SpannableString(name);
1526                 for (HighlightSequence highlightSequence : mNameHighlightSequence) {
1527                     mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start,
1528                             highlightSequence.end);
1529                 }
1530                 name = spannableName;
1531             }
1532         } else {
1533             name = mUnknownNameText;
1534         }
1535         setMarqueeText(getNameTextView(), name);
1536 
1537         if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
1538             // Give the text-to-speech engine a hint that it's a phone number
1539             mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
1540             mNameTextView.setContentDescription(
1541                     PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
1542         } else {
1543             // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
1544             // of the name into two separate parts.
1545             mNameTextView.setContentDescription(name.toString());
1546         }
1547     }
1548 
hideCheckBox()1549     public void hideCheckBox() {
1550         if (mCheckBox != null) {
1551             removeView(mCheckBox);
1552             mCheckBox = null;
1553         }
1554     }
1555 
hideDeleteImageButton()1556     public void hideDeleteImageButton() {
1557         if (mDeleteImageButton != null) {
1558             removeView(mDeleteImageButton);
1559             mDeleteImageButton = null;
1560         }
1561     }
1562 
hideDisplayName()1563     public void hideDisplayName() {
1564         if (mNameTextView != null) {
1565             removeView(mNameTextView);
1566             mNameTextView = null;
1567         }
1568     }
1569 
showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)1570     public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
1571         cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
1572         int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
1573         if (phoneticNameSize != 0) {
1574             setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
1575         } else {
1576             setPhoneticName(null, 0);
1577         }
1578     }
1579 
hidePhoneticName()1580     public void hidePhoneticName() {
1581         if (mPhoneticNameTextView != null) {
1582             removeView(mPhoneticNameTextView);
1583             mPhoneticNameTextView = null;
1584         }
1585     }
1586 
1587     /**
1588      * Sets the proper icon (star or presence or nothing) and/or status message.
1589      */
showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)1590     public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
1591             int contactStatusColumnIndex) {
1592         Drawable icon = null;
1593         int presence = 0;
1594         if (!cursor.isNull(presenceColumnIndex)) {
1595             presence = cursor.getInt(presenceColumnIndex);
1596             icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
1597         }
1598         setPresence(icon);
1599 
1600         String statusMessage = null;
1601         if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
1602             statusMessage = cursor.getString(contactStatusColumnIndex);
1603         }
1604         // If there is no status message from the contact, but there was a presence value, then use
1605         // the default status message string
1606         if (statusMessage == null && presence != 0) {
1607             statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
1608         }
1609         setStatus(statusMessage);
1610     }
1611 
1612     /**
1613      * Shows search snippet for email and phone number matches.
1614      */
showSnippet(Cursor cursor, String query, int snippetColumn)1615     public void showSnippet(Cursor cursor, String query, int snippetColumn) {
1616         // TODO: this does not properly handle phone numbers with control characters
1617         // For example if the phone number is 444-5555, the search query 4445 will match the
1618         // number since we normalize it before querying CP2 but the snippet will fail since
1619         // the portion to be highlighted is 444-5 not 4445.
1620         final String snippet = cursor.getString(snippetColumn);
1621         if (snippet == null) {
1622             setSnippet(null);
1623             return;
1624         }
1625         final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0
1626                 ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null;
1627         if (snippet.equals(displayName)) {
1628             // If the snippet exactly matches the display name (i.e. the phone number or email
1629             // address is being used as the display name) then no snippet is necessary
1630             setSnippet(null);
1631             return;
1632         }
1633         // Show the snippet with the part of the query that matched it
1634         setSnippet(updateSnippet(snippet, query, displayName));
1635     }
1636 
1637     /**
1638      * Shows search snippet.
1639      */
showSnippet(Cursor cursor, int summarySnippetColumnIndex)1640     public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
1641         if (cursor.getColumnCount() <= summarySnippetColumnIndex
1642             || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
1643             setSnippet(null);
1644             return;
1645         }
1646 
1647         String snippet = cursor.getString(summarySnippetColumnIndex);
1648 
1649         // Do client side snippeting if provider didn't do it
1650         final Bundle extras = cursor.getExtras();
1651         if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
1652 
1653             final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
1654 
1655             String displayName = null;
1656             int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
1657             if (displayNameIndex >= 0) {
1658                 displayName = cursor.getString(displayNameIndex);
1659             }
1660 
1661             snippet = updateSnippet(snippet, query, displayName);
1662 
1663         } else {
1664             if (snippet != null) {
1665                 int from = 0;
1666                 int to = snippet.length();
1667                 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
1668                 if (start == -1) {
1669                     snippet = null;
1670                 } else {
1671                     int firstNl = snippet.lastIndexOf('\n', start);
1672                     if (firstNl != -1) {
1673                         from = firstNl + 1;
1674                     }
1675                     int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
1676                     if (end != -1) {
1677                         int lastNl = snippet.indexOf('\n', end);
1678                         if (lastNl != -1) {
1679                             to = lastNl;
1680                         }
1681                     }
1682 
1683                     StringBuilder sb = new StringBuilder();
1684                     for (int i = from; i < to; i++) {
1685                         char c = snippet.charAt(i);
1686                         if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
1687                                 c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
1688                             sb.append(c);
1689                         }
1690                     }
1691                     snippet = sb.toString();
1692                 }
1693             }
1694         }
1695 
1696         setSnippet(snippet);
1697     }
1698 
1699     /**
1700      * Used for deferred snippets from the database. The contents come back as large strings which
1701      * need to be extracted for display.
1702      *
1703      * @param snippet The snippet from the database.
1704      * @param query The search query substring.
1705      * @param displayName The contact display name.
1706      * @return The proper snippet to display.
1707      */
updateSnippet(String snippet, String query, String displayName)1708     private String updateSnippet(String snippet, String query, String displayName) {
1709 
1710         if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
1711             return null;
1712         }
1713         query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
1714 
1715         // If the display name already contains the query term, return empty - snippets should
1716         // not be needed in that case.
1717         if (!TextUtils.isEmpty(displayName)) {
1718             final String lowerDisplayName = displayName.toLowerCase();
1719             final List<String> nameTokens = split(lowerDisplayName);
1720             for (String nameToken : nameTokens) {
1721                 if (nameToken.startsWith(query)) {
1722                     return null;
1723                 }
1724             }
1725         }
1726 
1727         // The snippet may contain multiple data lines.
1728         // Show the first line that matches the query.
1729         final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
1730 
1731         if (matched != null && matched.line != null) {
1732             // Tokenize for long strings since the match may be at the end of it.
1733             // Skip this part for short strings since the whole string will be displayed.
1734             // Most contact strings are short so the snippetize method will be called infrequently.
1735             final int lengthThreshold = getResources().getInteger(
1736                     R.integer.snippet_length_before_tokenize);
1737             if (matched.line.length() > lengthThreshold) {
1738                 return snippetize(matched.line, matched.startIndex, lengthThreshold);
1739             } else {
1740                 return matched.line;
1741             }
1742         }
1743 
1744         // No match found.
1745         return null;
1746     }
1747 
snippetize(String line, int matchIndex, int maxLength)1748     private String snippetize(String line, int matchIndex, int maxLength) {
1749         // Show up to maxLength characters. But we only show full tokens so show the last full token
1750         // up to maxLength characters. So as many starting tokens as possible before trying ending
1751         // tokens.
1752         int remainingLength = maxLength;
1753         int tempRemainingLength = remainingLength;
1754 
1755         // Start the end token after the matched query.
1756         int index = matchIndex;
1757         int endTokenIndex = index;
1758 
1759         // Find the match token first.
1760         while (index < line.length()) {
1761             if (!Character.isLetterOrDigit(line.charAt(index))) {
1762                 endTokenIndex = index;
1763                 remainingLength = tempRemainingLength;
1764                 break;
1765             }
1766             tempRemainingLength--;
1767             index++;
1768         }
1769 
1770         // Find as much content before the match.
1771         index = matchIndex - 1;
1772         tempRemainingLength = remainingLength;
1773         int startTokenIndex = matchIndex;
1774         while (index > -1 && tempRemainingLength > 0) {
1775             if (!Character.isLetterOrDigit(line.charAt(index))) {
1776                 startTokenIndex = index;
1777                 remainingLength = tempRemainingLength;
1778             }
1779             tempRemainingLength--;
1780             index--;
1781         }
1782 
1783         index = endTokenIndex;
1784         tempRemainingLength = remainingLength;
1785         // Find remaining content at after match.
1786         while (index < line.length() && tempRemainingLength > 0) {
1787             if (!Character.isLetterOrDigit(line.charAt(index))) {
1788                 endTokenIndex = index;
1789             }
1790             tempRemainingLength--;
1791             index++;
1792         }
1793         // Append ellipse if there is content before or after.
1794         final StringBuilder sb = new StringBuilder();
1795         if (startTokenIndex > 0) {
1796             sb.append("...");
1797         }
1798         sb.append(line.substring(startTokenIndex, endTokenIndex));
1799         if (endTokenIndex < line.length()) {
1800             sb.append("...");
1801         }
1802         return sb.toString();
1803     }
1804 
1805     private static final Pattern SPLIT_PATTERN = Pattern.compile(
1806             "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
1807 
1808     /**
1809      * Helper method for splitting a string into tokens.  The lists passed in are populated with
1810      * the
1811      * tokens and offsets into the content of each token.  The tokenization function parses e-mail
1812      * addresses as a single token; otherwise it splits on any non-alphanumeric character.
1813      *
1814      * @param content Content to split.
1815      * @return List of token strings.
1816      */
split(String content)1817     private static List<String> split(String content) {
1818         final Matcher matcher = SPLIT_PATTERN.matcher(content);
1819         final ArrayList<String> tokens = Lists.newArrayList();
1820         while (matcher.find()) {
1821             tokens.add(matcher.group());
1822         }
1823         return tokens;
1824     }
1825 
1826     /**
1827      * Shows data element.
1828      */
showData(Cursor cursor, int dataColumnIndex)1829     public void showData(Cursor cursor, int dataColumnIndex) {
1830         cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
1831         setData(mDataBuffer.data, mDataBuffer.sizeCopied);
1832     }
1833 
setActivatedStateSupported(boolean flag)1834     public void setActivatedStateSupported(boolean flag) {
1835         this.mActivatedStateSupported = flag;
1836     }
1837 
setAdjustSelectionBoundsEnabled(boolean enabled)1838     public void setAdjustSelectionBoundsEnabled(boolean enabled) {
1839         mAdjustSelectionBoundsEnabled = enabled;
1840     }
1841 
1842     @Override
requestLayout()1843     public void requestLayout() {
1844         // We will assume that once measured this will not need to resize
1845         // itself, so there is no need to pass the layout request to the parent
1846         // view (ListView).
1847         forceLayout();
1848     }
1849 
setPhotoPosition(PhotoPosition photoPosition)1850     public void setPhotoPosition(PhotoPosition photoPosition) {
1851         mPhotoPosition = photoPosition;
1852     }
1853 
getPhotoPosition()1854     public PhotoPosition getPhotoPosition() {
1855         return mPhotoPosition;
1856     }
1857 
1858     /**
1859      * Set drawable resources directly for the drawable resource of the photo view.
1860      *
1861      * @param drawableId Id of drawable resource.
1862      */
setDrawableResource(int drawableId)1863     public void setDrawableResource(int drawableId) {
1864         ImageView photo = getPhotoView();
1865         photo.setScaleType(ImageView.ScaleType.CENTER);
1866         final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
1867         final int iconColor =
1868                 ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
1869         if (CompatUtils.isLollipopCompatible()) {
1870             photo.setImageDrawable(drawable);
1871             photo.setImageTintList(ColorStateList.valueOf(iconColor));
1872         } else {
1873             final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
1874             DrawableCompat.setTint(drawableWrapper, iconColor);
1875             photo.setImageDrawable(drawableWrapper);
1876         }
1877     }
1878 
1879     @Override
onTouchEvent(MotionEvent event)1880     public boolean onTouchEvent(MotionEvent event) {
1881         final float x = event.getX();
1882         final float y = event.getY();
1883         // If the touch event's coordinates are not within the view's header, then delegate
1884         // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
1885         // and ignore the touch event.
1886         if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
1887             return super.onTouchEvent(event);
1888         } else {
1889             return true;
1890         }
1891     }
1892 
pointIsInView(float localX, float localY)1893     private final boolean pointIsInView(float localX, float localY) {
1894         return localX >= mLeftOffset && localX < mRightOffset
1895                 && localY >= 0 && localY < (getBottom() - getTop());
1896     }
1897 }
1898