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