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