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