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