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