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