• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.contacts.list;
18 
19 import com.android.contacts.ContactPresenceIconUtil;
20 import com.android.contacts.ContactStatusUtil;
21 import com.android.contacts.R;
22 import com.android.contacts.format.PrefixHighlighter;
23 
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.TypedArray;
27 import android.database.CharArrayBuffer;
28 import android.database.Cursor;
29 import android.graphics.Canvas;
30 import android.graphics.Color;
31 import android.graphics.Rect;
32 import android.graphics.Typeface;
33 import android.graphics.drawable.Drawable;
34 import android.os.Bundle;
35 import android.provider.ContactsContract;
36 import android.provider.ContactsContract.Contacts;
37 import android.text.Spannable;
38 import android.text.SpannableString;
39 import android.text.TextUtils;
40 import android.text.TextUtils.TruncateAt;
41 import android.util.AttributeSet;
42 import android.util.TypedValue;
43 import android.view.Gravity;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.AbsListView.SelectionBoundsAdjuster;
47 import android.widget.ImageView;
48 import android.widget.ImageView.ScaleType;
49 import android.widget.QuickContactBadge;
50 import android.widget.TextView;
51 
52 /**
53  * A custom view for an item in the contact list.
54  * The view contains the contact's photo, a set of text views (for name, status, etc...) and
55  * icons for presence and call.
56  * The view uses no XML file for layout and all the measurements and layouts are done
57  * in the onMeasure and onLayout methods.
58  *
59  * The layout puts the contact's photo on the right side of the view, the call icon (if present)
60  * to the left of the photo, the text lines are aligned to the left and the presence icon (if
61  * present) is set to the left of the status line.
62  *
63  * The layout also supports a header (used as a header of a group of contacts) that is above the
64  * contact's data and a divider between contact view.
65  */
66 
67 public class ContactListItemView extends ViewGroup
68         implements SelectionBoundsAdjuster {
69 
70     private static final int QUICK_CONTACT_BADGE_STYLE =
71             com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
72 
73     // Style values for layout and appearance
74     private final int mPreferredHeight;
75     private final int mVerticalDividerMargin;
76     private final int mGapBetweenImageAndText;
77     private final int mGapBetweenLabelAndData;
78     private final int mPresenceIconMargin;
79     private final int mPresenceIconSize;
80     private final int mHeaderTextColor;
81     private final int mHeaderTextIndent;
82     private final int mHeaderTextSize;
83     private final int mHeaderUnderlineHeight;
84     private final int mHeaderUnderlineColor;
85     private final int mCountViewTextSize;
86     private final int mContactsCountTextColor;
87     private final int mTextIndent;
88     private Drawable mActivatedBackgroundDrawable;
89 
90     /**
91      * Used with {@link #mLabelView}, specifying the width ratio between label and data.
92      */
93     private final int mLabelViewWidthWeight;
94     /**
95      * Used with {@link #mDataView}, specifying the width ratio between label and data.
96      */
97     private final int mDataViewWidthWeight;
98 
99     // Will be used with adjustListItemSelectionBounds().
100     private int mSelectionBoundsMarginLeft;
101     private int mSelectionBoundsMarginRight;
102 
103     // Horizontal divider between contact views.
104     private boolean mHorizontalDividerVisible = true;
105     private Drawable mHorizontalDividerDrawable;
106     private int mHorizontalDividerHeight;
107 
108     /**
109      * Where to put contact photo. This affects the other Views' layout or look-and-feel.
110      */
111     public enum PhotoPosition {
112         LEFT,
113         RIGHT
114     }
115     public static final PhotoPosition DEFAULT_PHOTO_POSITION = PhotoPosition.RIGHT;
116     private PhotoPosition mPhotoPosition = DEFAULT_PHOTO_POSITION;
117 
118     // Vertical divider between the call icon and the text.
119     private boolean mVerticalDividerVisible;
120     private Drawable mVerticalDividerDrawable;
121     private int mVerticalDividerWidth;
122 
123     // Header layout data
124     private boolean mHeaderVisible;
125     private View mHeaderDivider;
126     private int mHeaderBackgroundHeight;
127     private TextView mHeaderTextView;
128 
129     // The views inside the contact view
130     private boolean mQuickContactEnabled = true;
131     private QuickContactBadge mQuickContact;
132     private ImageView mPhotoView;
133     private TextView mNameTextView;
134     private TextView mPhoneticNameTextView;
135     private DontPressWithParentImageView mCallButton;
136     private TextView mLabelView;
137     private TextView mDataView;
138     private TextView mSnippetView;
139     private TextView mStatusView;
140     private TextView mCountView;
141     private ImageView mPresenceIcon;
142 
143     private ColorStateList mSecondaryTextColor;
144 
145     private char[] mHighlightedPrefix;
146 
147     private int mDefaultPhotoViewSize;
148     /**
149      * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
150      * to align other data in this View.
151      */
152     private int mPhotoViewWidth;
153     /**
154      * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
155      */
156     private int mPhotoViewHeight;
157 
158     /**
159      * Only effective when {@link #mPhotoView} is null.
160      * When true all the Views on the right side of the photo should have horizontal padding on
161      * those left assuming there is a photo.
162      */
163     private boolean mKeepHorizontalPaddingForPhotoView;
164     /**
165      * Only effective when {@link #mPhotoView} is null.
166      */
167     private boolean mKeepVerticalPaddingForPhotoView;
168 
169     /**
170      * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
171      * False indicates those values should be updated before being used in position calculation.
172      */
173     private boolean mPhotoViewWidthAndHeightAreReady = false;
174 
175     private int mNameTextViewHeight;
176     private int mPhoneticNameTextViewHeight;
177     private int mLabelViewHeight;
178     private int mDataViewHeight;
179     private int mSnippetTextViewHeight;
180     private int mStatusTextViewHeight;
181 
182     // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
183     // same row.
184     private int mLabelAndDataViewMaxHeight;
185 
186     // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
187     // more efficient for each case or in general, and simplify the whole implementation.
188     // Note: if we're sure MARQUEE will be used every time, there's no reason to use
189     // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
190     // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
191     // TextView without any modification.
192     private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
193     private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
194 
195     private boolean mActivatedStateSupported;
196 
197     private Rect mBoundsWithoutHeader = new Rect();
198 
199     /** A helper used to highlight a prefix in a text field. */
200     private PrefixHighlighter mPrefixHighlighter;
201     private CharSequence mUnknownNameText;
202 
203     /**
204      * Special class to allow the parent to be pressed without being pressed itself.
205      * This way the line of a tab can be pressed, but the image itself is not.
206      */
207     // TODO: understand this
208     private static class DontPressWithParentImageView extends ImageView {
209 
DontPressWithParentImageView(Context context, AttributeSet attrs)210         public DontPressWithParentImageView(Context context, AttributeSet attrs) {
211             super(context, attrs);
212         }
213 
214         @Override
setPressed(boolean pressed)215         public void setPressed(boolean pressed) {
216             // If the parent is pressed, do not set to pressed.
217             if (pressed && ((View) getParent()).isPressed()) {
218                 return;
219             }
220             super.setPressed(pressed);
221         }
222     }
223 
ContactListItemView(Context context, AttributeSet attrs)224     public ContactListItemView(Context context, AttributeSet attrs) {
225         super(context, attrs);
226         mContext = context;
227 
228         // Read all style values
229         TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
230         mPreferredHeight = a.getDimensionPixelSize(
231                 R.styleable.ContactListItemView_list_item_height, 0);
232         mActivatedBackgroundDrawable = a.getDrawable(
233                 R.styleable.ContactListItemView_activated_background);
234         mHorizontalDividerDrawable = a.getDrawable(
235                 R.styleable.ContactListItemView_list_item_divider);
236         mVerticalDividerMargin = a.getDimensionPixelOffset(
237                 R.styleable.ContactListItemView_list_item_vertical_divider_margin, 0);
238 
239         mGapBetweenImageAndText = a.getDimensionPixelOffset(
240                 R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 0);
241         mGapBetweenLabelAndData = a.getDimensionPixelOffset(
242                 R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0);
243         mPresenceIconMargin = a.getDimensionPixelOffset(
244                 R.styleable.ContactListItemView_list_item_presence_icon_margin, 4);
245         mPresenceIconSize = a.getDimensionPixelOffset(
246                 R.styleable.ContactListItemView_list_item_presence_icon_size, 16);
247         mDefaultPhotoViewSize = a.getDimensionPixelOffset(
248                 R.styleable.ContactListItemView_list_item_photo_size, 0);
249         mHeaderTextIndent = a.getDimensionPixelOffset(
250                 R.styleable.ContactListItemView_list_item_header_text_indent, 0);
251         mHeaderTextColor = a.getColor(
252                 R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK);
253         mHeaderTextSize = a.getDimensionPixelSize(
254                 R.styleable.ContactListItemView_list_item_header_text_size, 12);
255         mHeaderBackgroundHeight = a.getDimensionPixelSize(
256                 R.styleable.ContactListItemView_list_item_header_height, 30);
257         mHeaderUnderlineHeight = a.getDimensionPixelSize(
258                 R.styleable.ContactListItemView_list_item_header_underline_height, 1);
259         mHeaderUnderlineColor = a.getColor(
260                 R.styleable.ContactListItemView_list_item_header_underline_color, 0);
261         mTextIndent = a.getDimensionPixelOffset(
262                 R.styleable.ContactListItemView_list_item_text_indent, 0);
263         mCountViewTextSize = a.getDimensionPixelSize(
264                 R.styleable.ContactListItemView_list_item_contacts_count_text_size, 12);
265         mContactsCountTextColor = a.getColor(
266                 R.styleable.ContactListItemView_list_item_contacts_count_text_color, Color.BLACK);
267         mDataViewWidthWeight = a.getInteger(
268                 R.styleable.ContactListItemView_list_item_data_width_weight, 5);
269         mLabelViewWidthWeight = a.getInteger(
270                 R.styleable.ContactListItemView_list_item_label_width_weight, 3);
271 
272         setPadding(
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         final int prefixHighlightColor = a.getColor(
283                 R.styleable.ContactListItemView_list_item_prefix_highlight_color, Color.GREEN);
284         mPrefixHighlighter = new PrefixHighlighter(prefixHighlightColor);
285         a.recycle();
286 
287         a = getContext().obtainStyledAttributes(android.R.styleable.Theme);
288         mSecondaryTextColor = a.getColorStateList(android.R.styleable.Theme_textColorSecondary);
289         a.recycle();
290 
291         mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
292 
293         if (mActivatedBackgroundDrawable != null) {
294             mActivatedBackgroundDrawable.setCallback(this);
295         }
296     }
297 
setUnknownNameText(CharSequence unknownNameText)298     public void setUnknownNameText(CharSequence unknownNameText) {
299         mUnknownNameText = unknownNameText;
300     }
301 
setQuickContactEnabled(boolean flag)302     public void setQuickContactEnabled(boolean flag) {
303         mQuickContactEnabled = flag;
304     }
305 
306     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)307     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
308         // We will match parent's width and wrap content vertically, but make sure
309         // height is no less than listPreferredItemHeight.
310         final int specWidth = resolveSize(0, widthMeasureSpec);
311         final int preferredHeight;
312         if (mHorizontalDividerVisible) {
313             preferredHeight = mPreferredHeight + mHorizontalDividerHeight;
314         } else {
315             preferredHeight = mPreferredHeight;
316         }
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         final 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         // Go over all visible text views and measure actual width of each of them.
339         // Also calculate their heights to get the total height for this entire view.
340 
341         if (isVisible(mNameTextView)) {
342             mNameTextView.measure(
343                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
344                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
345             mNameTextViewHeight = mNameTextView.getMeasuredHeight();
346         }
347 
348         if (isVisible(mPhoneticNameTextView)) {
349             mPhoneticNameTextView.measure(
350                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
351                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
352             mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
353         }
354 
355         // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
356         // we should ellipsize both using appropriate ratio.
357         final int dataWidth;
358         final int labelWidth;
359         if (isVisible(mDataView)) {
360             if (isVisible(mLabelView)) {
361                 final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
362                 dataWidth = ((totalWidth * mDataViewWidthWeight)
363                         / (mDataViewWidthWeight + mLabelViewWidthWeight));
364                 labelWidth = ((totalWidth * mLabelViewWidthWeight) /
365                         (mDataViewWidthWeight + mLabelViewWidthWeight));
366             } else {
367                 dataWidth = effectiveWidth;
368                 labelWidth = 0;
369             }
370         } else {
371             dataWidth = 0;
372             if (isVisible(mLabelView)) {
373                 labelWidth = effectiveWidth;
374             } else {
375                 labelWidth = 0;
376             }
377         }
378 
379         if (isVisible(mDataView)) {
380             mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
381                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
382             mDataViewHeight = mDataView.getMeasuredHeight();
383         }
384 
385         if (isVisible(mLabelView)) {
386             // For performance reason we don't want AT_MOST usually, but when the picture is
387             // on right, we need to use it anyway because mDataView is next to mLabelView.
388             final int mode = (mPhotoPosition == PhotoPosition.LEFT
389                     ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST);
390             mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, mode),
391                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
392             mLabelViewHeight = mLabelView.getMeasuredHeight();
393         }
394         mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
395 
396         if (isVisible(mSnippetView)) {
397             mSnippetView.measure(
398                     MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
399                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
400             mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
401         }
402 
403         // Status view height is the biggest of the text view and the presence icon
404         if (isVisible(mPresenceIcon)) {
405             mPresenceIcon.measure(
406                     MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
407                     MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
408             mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
409         }
410 
411         if (isVisible(mStatusView)) {
412             // Presence and status are in a same row, so status will be affected by icon size.
413             final int statusWidth;
414             if (isVisible(mPresenceIcon)) {
415                 statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
416                         - mPresenceIconMargin);
417             } else {
418                 statusWidth = effectiveWidth;
419             }
420             mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
421                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
422             mStatusTextViewHeight =
423                     Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
424         }
425 
426         // Calculate height including padding.
427         int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
428                 mLabelAndDataViewMaxHeight +
429                 mSnippetTextViewHeight + mStatusTextViewHeight);
430 
431         if (isVisible(mCallButton)) {
432             mCallButton.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
433                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
434         }
435 
436         // Make sure the height is at least as high as the photo
437         height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
438 
439         // Add horizontal divider height
440         if (mHorizontalDividerVisible) {
441             height += mHorizontalDividerHeight;
442         }
443 
444         // Make sure height is at least the preferred height
445         height = Math.max(height, preferredHeight);
446 
447         // Add the height of the header if visible
448         if (mHeaderVisible) {
449             mHeaderTextView.measure(
450                     MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.EXACTLY),
451                     MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
452             if (mCountView != null) {
453                 mCountView.measure(
454                         MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.AT_MOST),
455                         MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
456             }
457             mHeaderBackgroundHeight = Math.max(mHeaderBackgroundHeight,
458                     mHeaderTextView.getMeasuredHeight());
459             height += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
460         }
461 
462         setMeasuredDimension(specWidth, height);
463     }
464 
465     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)466     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
467         final int height = bottom - top;
468         final int width = right - left;
469 
470         // Determine the vertical bounds by laying out the header first.
471         int topBound = 0;
472         int bottomBound = height;
473         int leftBound = getPaddingLeft();
474         int rightBound = width - getPaddingRight();
475 
476         // Put the header in the top of the contact view (Text + underline view)
477         if (mHeaderVisible) {
478             mHeaderTextView.layout(leftBound + mHeaderTextIndent,
479                     0,
480                     rightBound,
481                     mHeaderBackgroundHeight);
482             if (mCountView != null) {
483                 mCountView.layout(rightBound - mCountView.getMeasuredWidth(),
484                         0,
485                         rightBound,
486                         mHeaderBackgroundHeight);
487             }
488             mHeaderDivider.layout(leftBound,
489                     mHeaderBackgroundHeight,
490                     rightBound,
491                     mHeaderBackgroundHeight + mHeaderUnderlineHeight);
492             topBound += (mHeaderBackgroundHeight + mHeaderUnderlineHeight);
493         }
494 
495         // Put horizontal divider at the bottom
496         if (mHorizontalDividerVisible) {
497             mHorizontalDividerDrawable.setBounds(
498                     leftBound,
499                     height - mHorizontalDividerHeight,
500                     rightBound,
501                     height);
502             bottomBound -= mHorizontalDividerHeight;
503         }
504 
505         mBoundsWithoutHeader.set(0, topBound, width, bottomBound);
506 
507         if (mActivatedStateSupported && isActivated()) {
508             mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
509         }
510 
511         final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
512         if (mPhotoPosition == PhotoPosition.LEFT) {
513             // Photo is the left most view. All the other Views should on the right of the photo.
514             if (photoView != null) {
515                 // Center the photo vertically
516                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
517                 photoView.layout(
518                         leftBound,
519                         photoTop,
520                         leftBound + mPhotoViewWidth,
521                         photoTop + mPhotoViewHeight);
522                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
523             } else if (mKeepHorizontalPaddingForPhotoView) {
524                 // Draw nothing but keep the padding.
525                 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
526             }
527         } else {
528             // Photo is the right most view. Right bound should be adjusted that way.
529             if (photoView != null) {
530                 // Center the photo vertically
531                 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
532                 photoView.layout(
533                         rightBound - mPhotoViewWidth,
534                         photoTop,
535                         rightBound,
536                         photoTop + mPhotoViewHeight);
537                 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
538             }
539 
540             // Add indent between left-most padding and texts.
541             leftBound += mTextIndent;
542         }
543 
544         // Layout the call button.
545         rightBound = layoutRightSide(height, topBound, bottomBound, rightBound);
546 
547         // Center text vertically
548         final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
549                 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
550         int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
551 
552         // Layout all text view and presence icon
553         // Put name TextView first
554         if (isVisible(mNameTextView)) {
555             mNameTextView.layout(leftBound,
556                     textTopBound,
557                     rightBound,
558                     textTopBound + mNameTextViewHeight);
559             textTopBound += mNameTextViewHeight;
560         }
561 
562         // Presence and status
563         int statusLeftBound = leftBound;
564         if (isVisible(mPresenceIcon)) {
565             int iconWidth = mPresenceIcon.getMeasuredWidth();
566             mPresenceIcon.layout(
567                     leftBound,
568                     textTopBound,
569                     leftBound + iconWidth,
570                     textTopBound + mStatusTextViewHeight);
571             statusLeftBound += (iconWidth + mPresenceIconMargin);
572         }
573 
574         if (isVisible(mStatusView)) {
575             mStatusView.layout(statusLeftBound,
576                     textTopBound,
577                     rightBound,
578                     textTopBound + mStatusTextViewHeight);
579         }
580 
581         if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
582             textTopBound += mStatusTextViewHeight;
583         }
584 
585         // Rest of text views
586         int dataLeftBound = leftBound;
587         if (isVisible(mPhoneticNameTextView)) {
588             mPhoneticNameTextView.layout(leftBound,
589                     textTopBound,
590                     rightBound,
591                     textTopBound + mPhoneticNameTextViewHeight);
592             textTopBound += mPhoneticNameTextViewHeight;
593         }
594 
595         // Label and Data align bottom.
596         if (isVisible(mLabelView)) {
597             if (mPhotoPosition == PhotoPosition.LEFT) {
598                 // When photo is on left, label is placed on the right edge of the list item.
599                 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
600                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
601                         rightBound,
602                         textTopBound + mLabelAndDataViewMaxHeight);
603                 rightBound -= mLabelView.getMeasuredWidth();
604             } else {
605                 // When photo is on right, label is placed on the left of data view.
606                 dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
607                 mLabelView.layout(leftBound,
608                         textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
609                         dataLeftBound,
610                         textTopBound + mLabelAndDataViewMaxHeight);
611                 dataLeftBound += mGapBetweenLabelAndData;
612             }
613         }
614 
615         if (isVisible(mDataView)) {
616             mDataView.layout(dataLeftBound,
617                     textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
618                     rightBound,
619                     textTopBound + mLabelAndDataViewMaxHeight);
620         }
621         if (isVisible(mLabelView) || isVisible(mDataView)) {
622             textTopBound += mLabelAndDataViewMaxHeight;
623         }
624 
625         if (isVisible(mSnippetView)) {
626             mSnippetView.layout(leftBound,
627                     textTopBound,
628                     rightBound,
629                     textTopBound + mSnippetTextViewHeight);
630         }
631     }
632 
633     /**
634      * Performs layout of the right side of the view
635      *
636      * @return new right boundary
637      */
layoutRightSide(int height, int topBound, int bottomBound, int rightBound)638     protected int layoutRightSide(int height, int topBound, int bottomBound, int rightBound) {
639         // Put call button and vertical divider
640         if (isVisible(mCallButton)) {
641             int buttonWidth = mCallButton.getMeasuredWidth();
642             rightBound -= buttonWidth;
643             mCallButton.layout(
644                     rightBound,
645                     topBound,
646                     rightBound + buttonWidth,
647                     height - mHorizontalDividerHeight);
648             mVerticalDividerVisible = true;
649             ensureVerticalDivider();
650             rightBound -= mVerticalDividerWidth;
651             mVerticalDividerDrawable.setBounds(
652                     rightBound,
653                     topBound + mVerticalDividerMargin,
654                     rightBound + mVerticalDividerWidth,
655                     height - mVerticalDividerMargin);
656         } else {
657             mVerticalDividerVisible = false;
658         }
659 
660         return rightBound;
661     }
662 
663     @Override
adjustListItemSelectionBounds(Rect bounds)664     public void adjustListItemSelectionBounds(Rect bounds) {
665         bounds.top += mBoundsWithoutHeader.top;
666         bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
667         bounds.left += mSelectionBoundsMarginLeft;
668         bounds.right -= mSelectionBoundsMarginRight;
669     }
670 
isVisible(View view)671     protected boolean isVisible(View view) {
672         return view != null && view.getVisibility() == View.VISIBLE;
673     }
674 
675     /**
676      * Loads the drawable for the vertical divider if it has not yet been loaded.
677      */
ensureVerticalDivider()678     private void ensureVerticalDivider() {
679         if (mVerticalDividerDrawable == null) {
680             mVerticalDividerDrawable = mContext.getResources().getDrawable(
681                     R.drawable.divider_vertical_dark);
682             mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth();
683         }
684     }
685 
686     /**
687      * Extracts width and height from the style
688      */
ensurePhotoViewSize()689     private void ensurePhotoViewSize() {
690         if (!mPhotoViewWidthAndHeightAreReady) {
691             if (mQuickContactEnabled) {
692                 TypedArray a = mContext.obtainStyledAttributes(null,
693                         com.android.internal.R.styleable.ViewGroup_Layout,
694                         QUICK_CONTACT_BADGE_STYLE, 0);
695                 mPhotoViewWidth = a.getLayoutDimension(
696                         android.R.styleable.ViewGroup_Layout_layout_width,
697                         ViewGroup.LayoutParams.WRAP_CONTENT);
698                 mPhotoViewHeight = a.getLayoutDimension(
699                         android.R.styleable.ViewGroup_Layout_layout_height,
700                         ViewGroup.LayoutParams.WRAP_CONTENT);
701                 a.recycle();
702             } else if (mPhotoView != null) {
703                 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
704             } else {
705                 final int defaultPhotoViewSize = getDefaultPhotoViewSize();
706                 mPhotoViewWidth = mKeepHorizontalPaddingForPhotoView ? defaultPhotoViewSize : 0;
707                 mPhotoViewHeight = mKeepVerticalPaddingForPhotoView ? defaultPhotoViewSize : 0;
708             }
709 
710             mPhotoViewWidthAndHeightAreReady = true;
711         }
712     }
713 
setDefaultPhotoViewSize(int pixels)714     protected void setDefaultPhotoViewSize(int pixels) {
715         mDefaultPhotoViewSize = pixels;
716     }
717 
getDefaultPhotoViewSize()718     protected int getDefaultPhotoViewSize() {
719         return mDefaultPhotoViewSize;
720     }
721 
722     @Override
drawableStateChanged()723     protected void drawableStateChanged() {
724         super.drawableStateChanged();
725         if (mActivatedStateSupported) {
726             mActivatedBackgroundDrawable.setState(getDrawableState());
727         }
728     }
729 
730     @Override
verifyDrawable(Drawable who)731     protected boolean verifyDrawable(Drawable who) {
732         return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
733     }
734 
735     @Override
jumpDrawablesToCurrentState()736     public void jumpDrawablesToCurrentState() {
737         super.jumpDrawablesToCurrentState();
738         if (mActivatedStateSupported) {
739             mActivatedBackgroundDrawable.jumpToCurrentState();
740         }
741     }
742 
743     @Override
dispatchDraw(Canvas canvas)744     public void dispatchDraw(Canvas canvas) {
745         if (mActivatedStateSupported && isActivated()) {
746             mActivatedBackgroundDrawable.draw(canvas);
747         }
748         if (mHorizontalDividerVisible) {
749             mHorizontalDividerDrawable.draw(canvas);
750         }
751         if (mVerticalDividerVisible) {
752             mVerticalDividerDrawable.draw(canvas);
753         }
754 
755         super.dispatchDraw(canvas);
756     }
757 
758     /**
759      * Sets the flag that determines whether a divider should drawn at the bottom
760      * of the view.
761      */
setDividerVisible(boolean visible)762     public void setDividerVisible(boolean visible) {
763         mHorizontalDividerVisible = visible;
764     }
765 
766     /**
767      * Sets section header or makes it invisible if the title is null.
768      */
setSectionHeader(String title)769     public void setSectionHeader(String title) {
770         if (!TextUtils.isEmpty(title)) {
771             if (mHeaderTextView == null) {
772                 mHeaderTextView = new TextView(mContext);
773                 mHeaderTextView.setTextColor(mHeaderTextColor);
774                 mHeaderTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mHeaderTextSize);
775                 mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
776                 mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL);
777                 addView(mHeaderTextView);
778             }
779             if (mHeaderDivider == null) {
780                 mHeaderDivider = new View(mContext);
781                 mHeaderDivider.setBackgroundColor(mHeaderUnderlineColor);
782                 addView(mHeaderDivider);
783             }
784             setMarqueeText(mHeaderTextView, title);
785             mHeaderTextView.setVisibility(View.VISIBLE);
786             mHeaderDivider.setVisibility(View.VISIBLE);
787             mHeaderTextView.setAllCaps(true);
788             mHeaderVisible = true;
789         } else {
790             if (mHeaderTextView != null) {
791                 mHeaderTextView.setVisibility(View.GONE);
792             }
793             if (mHeaderDivider != null) {
794                 mHeaderDivider.setVisibility(View.GONE);
795             }
796             mHeaderVisible = false;
797         }
798     }
799 
800     /**
801      * Returns the quick contact badge, creating it if necessary.
802      */
getQuickContact()803     public QuickContactBadge getQuickContact() {
804         if (!mQuickContactEnabled) {
805             throw new IllegalStateException("QuickContact is disabled for this view");
806         }
807         if (mQuickContact == null) {
808             mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE);
809             if (mNameTextView != null) {
810                 mQuickContact.setContentDescription(mContext.getString(
811                         R.string.description_quick_contact_for, mNameTextView.getText()));
812             }
813 
814             addView(mQuickContact);
815             mPhotoViewWidthAndHeightAreReady = false;
816         }
817         return mQuickContact;
818     }
819 
820     /**
821      * Returns the photo view, creating it if necessary.
822      */
getPhotoView()823     public ImageView getPhotoView() {
824         if (mPhotoView == null) {
825             if (mQuickContactEnabled) {
826                 mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE);
827             } else {
828                 mPhotoView = new ImageView(mContext);
829             }
830             // Quick contact style used above will set a background - remove it
831             mPhotoView.setBackground(null);
832             addView(mPhotoView);
833             mPhotoViewWidthAndHeightAreReady = false;
834         }
835         return mPhotoView;
836     }
837 
838     /**
839      * Removes the photo view.
840      */
removePhotoView()841     public void removePhotoView() {
842         removePhotoView(false, true);
843     }
844 
845     /**
846      * Removes the photo view.
847      *
848      * @param keepHorizontalPadding True means data on the right side will have
849      *            padding on left, pretending there is still a photo view.
850      * @param keepVerticalPadding True means the View will have some height
851      *            enough for accommodating a photo view.
852      */
removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding)853     public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
854         mPhotoViewWidthAndHeightAreReady = false;
855         mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
856         mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
857         if (mPhotoView != null) {
858             removeView(mPhotoView);
859             mPhotoView = null;
860         }
861         if (mQuickContact != null) {
862             removeView(mQuickContact);
863             mQuickContact = null;
864         }
865     }
866 
867     /**
868      * Sets a word prefix that will be highlighted if encountered in fields like
869      * name and search snippet.
870      * <p>
871      * NOTE: must be all upper-case
872      */
setHighlightedPrefix(char[] upperCasePrefix)873     public void setHighlightedPrefix(char[] upperCasePrefix) {
874         mHighlightedPrefix = upperCasePrefix;
875     }
876 
877     /**
878      * Returns the text view for the contact name, creating it if necessary.
879      */
getNameTextView()880     public TextView getNameTextView() {
881         if (mNameTextView == null) {
882             mNameTextView = new TextView(mContext);
883             mNameTextView.setSingleLine(true);
884             mNameTextView.setEllipsize(getTextEllipsis());
885             mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
886             // Manually call setActivated() since this view may be added after the first
887             // setActivated() call toward this whole item view.
888             mNameTextView.setActivated(isActivated());
889             mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
890             addView(mNameTextView);
891         }
892         return mNameTextView;
893     }
894 
895     /**
896      * Adds or updates a text view for the phonetic name.
897      */
setPhoneticName(char[] text, int size)898     public void setPhoneticName(char[] text, int size) {
899         if (text == null || size == 0) {
900             if (mPhoneticNameTextView != null) {
901                 mPhoneticNameTextView.setVisibility(View.GONE);
902             }
903         } else {
904             getPhoneticNameTextView();
905             setMarqueeText(mPhoneticNameTextView, text, size);
906             mPhoneticNameTextView.setVisibility(VISIBLE);
907         }
908     }
909 
910     /**
911      * Returns the text view for the phonetic name, creating it if necessary.
912      */
getPhoneticNameTextView()913     public TextView getPhoneticNameTextView() {
914         if (mPhoneticNameTextView == null) {
915             mPhoneticNameTextView = new TextView(mContext);
916             mPhoneticNameTextView.setSingleLine(true);
917             mPhoneticNameTextView.setEllipsize(getTextEllipsis());
918             mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
919             mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
920             mPhoneticNameTextView.setActivated(isActivated());
921             addView(mPhoneticNameTextView);
922         }
923         return mPhoneticNameTextView;
924     }
925 
926     /**
927      * Adds or updates a text view for the data label.
928      */
setLabel(CharSequence text)929     public void setLabel(CharSequence text) {
930         if (TextUtils.isEmpty(text)) {
931             if (mLabelView != null) {
932                 mLabelView.setVisibility(View.GONE);
933             }
934         } else {
935             getLabelView();
936             setMarqueeText(mLabelView, text);
937             mLabelView.setVisibility(VISIBLE);
938         }
939     }
940 
941     /**
942      * Returns the text view for the data label, creating it if necessary.
943      */
getLabelView()944     public TextView getLabelView() {
945         if (mLabelView == null) {
946             mLabelView = new TextView(mContext);
947             mLabelView.setSingleLine(true);
948             mLabelView.setEllipsize(getTextEllipsis());
949             mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
950             if (mPhotoPosition == PhotoPosition.LEFT) {
951                 mLabelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mCountViewTextSize);
952                 mLabelView.setAllCaps(true);
953                 mLabelView.setGravity(Gravity.RIGHT);
954             } else {
955                 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
956             }
957             mLabelView.setActivated(isActivated());
958             addView(mLabelView);
959         }
960         return mLabelView;
961     }
962 
963     /**
964      * Adds or updates a text view for the data element.
965      */
setData(char[] text, int size)966     public void setData(char[] text, int size) {
967         if (text == null || size == 0) {
968             if (mDataView != null) {
969                 mDataView.setVisibility(View.GONE);
970             }
971         } else {
972             getDataView();
973             setMarqueeText(mDataView, text, size);
974             mDataView.setVisibility(VISIBLE);
975         }
976     }
977 
setMarqueeText(TextView textView, char[] text, int size)978     private void setMarqueeText(TextView textView, char[] text, int size) {
979         if (getTextEllipsis() == TruncateAt.MARQUEE) {
980             setMarqueeText(textView, new String(text, 0, size));
981         } else {
982             textView.setText(text, 0, size);
983         }
984     }
985 
setMarqueeText(TextView textView, CharSequence text)986     private void setMarqueeText(TextView textView, CharSequence text) {
987         if (getTextEllipsis() == TruncateAt.MARQUEE) {
988             // To show MARQUEE correctly (with END effect during non-active state), we need
989             // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
990             final SpannableString spannable = new SpannableString(text);
991             spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
992                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
993             textView.setText(spannable);
994         } else {
995             textView.setText(text);
996         }
997     }
998 
999     /**
1000      * Returns the text view for the data text, creating it if necessary.
1001      */
getDataView()1002     public TextView getDataView() {
1003         if (mDataView == null) {
1004             mDataView = new TextView(mContext);
1005             mDataView.setSingleLine(true);
1006             mDataView.setEllipsize(getTextEllipsis());
1007             mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
1008             mDataView.setActivated(isActivated());
1009             addView(mDataView);
1010         }
1011         return mDataView;
1012     }
1013 
1014     /**
1015      * Adds or updates a text view for the search snippet.
1016      */
setSnippet(String text)1017     public void setSnippet(String text) {
1018         if (TextUtils.isEmpty(text)) {
1019             if (mSnippetView != null) {
1020                 mSnippetView.setVisibility(View.GONE);
1021             }
1022         } else {
1023             mPrefixHighlighter.setText(getSnippetView(), text, mHighlightedPrefix);
1024             mSnippetView.setVisibility(VISIBLE);
1025         }
1026     }
1027 
1028     /**
1029      * Returns the text view for the search snippet, creating it if necessary.
1030      */
getSnippetView()1031     public TextView getSnippetView() {
1032         if (mSnippetView == null) {
1033             mSnippetView = new TextView(mContext);
1034             mSnippetView.setSingleLine(true);
1035             mSnippetView.setEllipsize(getTextEllipsis());
1036             mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
1037             mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
1038             mSnippetView.setActivated(isActivated());
1039             addView(mSnippetView);
1040         }
1041         return mSnippetView;
1042     }
1043 
1044     /**
1045      * Returns the text view for the status, creating it if necessary.
1046      */
getStatusView()1047     public TextView getStatusView() {
1048         if (mStatusView == null) {
1049             mStatusView = new TextView(mContext);
1050             mStatusView.setSingleLine(true);
1051             mStatusView.setEllipsize(getTextEllipsis());
1052             mStatusView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
1053             mStatusView.setTextColor(mSecondaryTextColor);
1054             mStatusView.setActivated(isActivated());
1055             addView(mStatusView);
1056         }
1057         return mStatusView;
1058     }
1059 
1060     /**
1061      * Returns the text view for the contacts count, creating it if necessary.
1062      */
getCountView()1063     public TextView getCountView() {
1064         if (mCountView == null) {
1065             mCountView = new TextView(mContext);
1066             mCountView.setSingleLine(true);
1067             mCountView.setEllipsize(getTextEllipsis());
1068             mCountView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
1069             mCountView.setTextColor(R.color.contact_count_text_color);
1070             addView(mCountView);
1071         }
1072         return mCountView;
1073     }
1074 
1075     /**
1076      * Adds or updates a text view for the contacts count.
1077      */
setCountView(CharSequence text)1078     public void setCountView(CharSequence text) {
1079         if (TextUtils.isEmpty(text)) {
1080             if (mCountView != null) {
1081                 mCountView.setVisibility(View.GONE);
1082             }
1083         } else {
1084             getCountView();
1085             setMarqueeText(mCountView, text);
1086             mCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCountViewTextSize);
1087             mCountView.setGravity(Gravity.CENTER_VERTICAL);
1088             mCountView.setTextColor(mContactsCountTextColor);
1089             mCountView.setVisibility(VISIBLE);
1090         }
1091     }
1092 
1093     /**
1094      * Adds or updates a text view for the status.
1095      */
setStatus(CharSequence text)1096     public void setStatus(CharSequence text) {
1097         if (TextUtils.isEmpty(text)) {
1098             if (mStatusView != null) {
1099                 mStatusView.setVisibility(View.GONE);
1100             }
1101         } else {
1102             getStatusView();
1103             setMarqueeText(mStatusView, text);
1104             mStatusView.setVisibility(VISIBLE);
1105         }
1106     }
1107 
1108     /**
1109      * Adds or updates the presence icon view.
1110      */
setPresence(Drawable icon)1111     public void setPresence(Drawable icon) {
1112         if (icon != null) {
1113             if (mPresenceIcon == null) {
1114                 mPresenceIcon = new ImageView(mContext);
1115                 addView(mPresenceIcon);
1116             }
1117             mPresenceIcon.setImageDrawable(icon);
1118             mPresenceIcon.setScaleType(ScaleType.CENTER);
1119             mPresenceIcon.setVisibility(View.VISIBLE);
1120         } else {
1121             if (mPresenceIcon != null) {
1122                 mPresenceIcon.setVisibility(View.GONE);
1123             }
1124         }
1125     }
1126 
getTextEllipsis()1127     private TruncateAt getTextEllipsis() {
1128         return TruncateAt.MARQUEE;
1129     }
1130 
showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder)1131     public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
1132         CharSequence name = cursor.getString(nameColumnIndex);
1133         if (!TextUtils.isEmpty(name)) {
1134             name = mPrefixHighlighter.apply(name, mHighlightedPrefix);
1135         } else {
1136             name = mUnknownNameText;
1137         }
1138         setMarqueeText(getNameTextView(), name);
1139 
1140         // Since the quick contact content description is derived from the display name and there is
1141         // no guarantee that when the quick contact is initialized the display name is already set,
1142         // do it here too.
1143         if (mQuickContact != null) {
1144             mQuickContact.setContentDescription(mContext.getString(
1145                     R.string.description_quick_contact_for, mNameTextView.getText()));
1146         }
1147     }
1148 
hideDisplayName()1149     public void hideDisplayName() {
1150         if (mNameTextView != null) {
1151             removeView(mNameTextView);
1152             mNameTextView = null;
1153         }
1154     }
1155 
showPhoneticName(Cursor cursor, int phoneticNameColumnIndex)1156     public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
1157         cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
1158         int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
1159         if (phoneticNameSize != 0) {
1160             setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
1161         } else {
1162             setPhoneticName(null, 0);
1163         }
1164     }
1165 
hidePhoneticName()1166     public void hidePhoneticName() {
1167         if (mPhoneticNameTextView != null) {
1168             removeView(mPhoneticNameTextView);
1169             mPhoneticNameTextView = null;
1170         }
1171     }
1172 
1173     /**
1174      * Sets the proper icon (star or presence or nothing) and/or status message.
1175      */
showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex)1176     public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
1177             int contactStatusColumnIndex) {
1178         Drawable icon = null;
1179         int presence = 0;
1180         if (!cursor.isNull(presenceColumnIndex)) {
1181             presence = cursor.getInt(presenceColumnIndex);
1182             icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
1183         }
1184         setPresence(icon);
1185 
1186         String statusMessage = null;
1187         if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
1188             statusMessage = cursor.getString(contactStatusColumnIndex);
1189         }
1190         // If there is no status message from the contact, but there was a presence value, then use
1191         // the default status message string
1192         if (statusMessage == null && presence != 0) {
1193             statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
1194         }
1195         setStatus(statusMessage);
1196     }
1197 
1198     /**
1199      * Shows search snippet.
1200      */
showSnippet(Cursor cursor, int summarySnippetColumnIndex)1201     public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
1202         if (cursor.getColumnCount() <= summarySnippetColumnIndex) {
1203             setSnippet(null);
1204             return;
1205         }
1206         String snippet;
1207         String columnContent = cursor.getString(summarySnippetColumnIndex);
1208 
1209         // Do client side snippeting if provider didn't do it
1210         Bundle extras = cursor.getExtras();
1211         if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
1212             int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
1213 
1214             snippet = ContactsContract.snippetize(columnContent,
1215                     displayNameIndex < 0 ? null : cursor.getString(displayNameIndex),
1216                             extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY),
1217                             DefaultContactListAdapter.SNIPPET_START_MATCH,
1218                             DefaultContactListAdapter.SNIPPET_END_MATCH,
1219                             DefaultContactListAdapter.SNIPPET_ELLIPSIS,
1220                             DefaultContactListAdapter.SNIPPET_MAX_TOKENS);
1221         } else {
1222             snippet = columnContent;
1223         }
1224 
1225         if (snippet != null) {
1226             int from = 0;
1227             int to = snippet.length();
1228             int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
1229             if (start == -1) {
1230                 snippet = null;
1231             } else {
1232                 int firstNl = snippet.lastIndexOf('\n', start);
1233                 if (firstNl != -1) {
1234                     from = firstNl + 1;
1235                 }
1236                 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
1237                 if (end != -1) {
1238                     int lastNl = snippet.indexOf('\n', end);
1239                     if (lastNl != -1) {
1240                         to = lastNl;
1241                     }
1242                 }
1243 
1244                 StringBuilder sb = new StringBuilder();
1245                 for (int i = from; i < to; i++) {
1246                     char c = snippet.charAt(i);
1247                     if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
1248                             c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
1249                         sb.append(c);
1250                     }
1251                 }
1252                 snippet = sb.toString();
1253             }
1254         }
1255         setSnippet(snippet);
1256     }
1257 
1258     /**
1259      * Shows data element (e.g. phone number).
1260      */
showData(Cursor cursor, int dataColumnIndex)1261     public void showData(Cursor cursor, int dataColumnIndex) {
1262         cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
1263         setData(mDataBuffer.data, mDataBuffer.sizeCopied);
1264     }
1265 
setActivatedStateSupported(boolean flag)1266     public void setActivatedStateSupported(boolean flag) {
1267         this.mActivatedStateSupported = flag;
1268     }
1269 
1270     @Override
requestLayout()1271     public void requestLayout() {
1272         // We will assume that once measured this will not need to resize
1273         // itself, so there is no need to pass the layout request to the parent
1274         // view (ListView).
1275         forceLayout();
1276     }
1277 
setPhotoPosition(PhotoPosition photoPosition)1278     public void setPhotoPosition(PhotoPosition photoPosition) {
1279         mPhotoPosition = photoPosition;
1280     }
1281 
getPhotoPosition()1282     public PhotoPosition getPhotoPosition() {
1283         return mPhotoPosition;
1284     }
1285 
1286     /**
1287      * Specifies left and right margin for selection bounds. See also
1288      * {@link #adjustListItemSelectionBounds(Rect)}.
1289      */
setSelectionBoundsHorizontalMargin(int left, int right)1290     public void setSelectionBoundsHorizontalMargin(int left, int right) {
1291         mSelectionBoundsMarginLeft = left;
1292         mSelectionBoundsMarginRight = right;
1293     }
1294 }
1295