ContactListItemView.java revision 5181dff5240a207fd04e0c065d978535c3e8744f
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.R;
21import com.android.contacts.widget.TextWithHighlighting;
22import com.android.contacts.widget.TextWithHighlightingFactory;
23
24import android.content.Context;
25import android.content.res.TypedArray;
26import android.database.CharArrayBuffer;
27import android.database.Cursor;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Rect;
31import android.graphics.Typeface;
32import android.graphics.drawable.Drawable;
33import android.provider.ContactsContract.Contacts;
34import android.text.SpannableString;
35import android.text.TextUtils;
36import android.text.TextUtils.TruncateAt;
37import android.text.style.ForegroundColorSpan;
38import android.util.AttributeSet;
39import android.view.Gravity;
40import android.view.View;
41import android.view.ViewGroup;
42import android.widget.AbsListView.SelectionBoundsAdjuster;
43import android.widget.ImageView;
44import android.widget.ImageView.ScaleType;
45import android.widget.QuickContactBadge;
46import android.widget.TextView;
47
48/**
49 * A custom view for an item in the contact list.
50 */
51public class ContactListItemView extends ViewGroup
52        implements SelectionBoundsAdjuster
53{
54
55    private static final int QUICK_CONTACT_BADGE_STYLE =
56            com.android.internal.R.attr.quickContactBadgeStyleWindowMedium;
57
58    protected final Context mContext;
59
60    private final int mPreferredHeight;
61    private final int mVerticalDividerMargin;
62    private final int mPaddingTop;
63    private final int mPaddingRight;
64    private final int mPaddingBottom;
65    private final int mPaddingLeft;
66    private final int mGapBetweenImageAndText;
67    private final int mGapBetweenLabelAndData;
68    private final int mCallButtonPadding;
69    private final int mPresenceIconMargin;
70    private final int mPrefixHightlightColor;
71    private final int mHeaderTextColor;
72    private final int mHeaderTextIndent;
73    private final int mHeaderTextSize;
74
75    private Drawable mActivatedBackgroundDrawable;
76
77    private boolean mHorizontalDividerVisible = true;
78    private Drawable mHorizontalDividerDrawable;
79    private int mHorizontalDividerHeight;
80
81    private boolean mVerticalDividerVisible;
82    private Drawable mVerticalDividerDrawable;
83    private int mVerticalDividerWidth;
84
85    private boolean mHeaderVisible;
86    private Drawable mHeaderBackgroundDrawable;
87    private int mHeaderBackgroundHeight;
88    private TextView mHeaderTextView;
89
90    private boolean mQuickContactEnabled = true;
91    private QuickContactBadge mQuickContact;
92    private ImageView mPhotoView;
93    private TextView mNameTextView;
94    private TextView mPhoneticNameTextView;
95    private DontPressWithParentImageView mCallButton;
96    private TextView mLabelView;
97    private TextView mDataView;
98    private TextView mSnippetView;
99    private ImageView mPresenceIcon;
100
101    private char[] mHighlightedPrefix;
102
103    private int mDefaultPhotoViewSize;
104    private int mPhotoViewWidth;
105    private int mPhotoViewHeight;
106    private int mLine1Height;
107    private int mLine2Height;
108    private int mLine3Height;
109    private int mLine4Height;
110
111    private OnClickListener mCallButtonClickListener;
112    private TextWithHighlightingFactory mTextWithHighlightingFactory;
113    private CharArrayBuffer mNameBuffer = new CharArrayBuffer(128);
114    private CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
115    private CharArrayBuffer mHighlightedTextBuffer = new CharArrayBuffer(128);
116    private TextWithHighlighting mTextWithHighlighting;
117    private CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
118
119    private CharSequence mUnknownNameText;
120
121    private boolean mActivatedStateSupported;
122
123    private ForegroundColorSpan mPrefixColorSpan;
124
125    private Rect mBoundsWithoutHeader = new Rect();
126
127    /**
128     * Special class to allow the parent to be pressed without being pressed itself.
129     * This way the line of a tab can be pressed, but the image itself is not.
130     */
131    // TODO: understand this
132    private static class DontPressWithParentImageView extends ImageView {
133
134        public DontPressWithParentImageView(Context context, AttributeSet attrs) {
135            super(context, attrs);
136        }
137
138        @Override
139        public void setPressed(boolean pressed) {
140            // If the parent is pressed, do not set to pressed.
141            if (pressed && ((View) getParent()).isPressed()) {
142                return;
143            }
144            super.setPressed(pressed);
145        }
146    }
147
148    public ContactListItemView(Context context, AttributeSet attrs) {
149        super(context, attrs);
150        mContext = context;
151
152        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
153        mPreferredHeight = a.getDimensionPixelSize(
154                R.styleable.ContactListItemView_list_item_height, 0);
155        mActivatedBackgroundDrawable = a.getDrawable(
156                R.styleable.ContactListItemView_activated_background);
157        mHeaderBackgroundDrawable = a.getDrawable(
158                R.styleable.ContactListItemView_section_header_background);
159        mHorizontalDividerDrawable = a.getDrawable(
160                R.styleable.ContactListItemView_list_item_divider);
161        mVerticalDividerMargin = a.getDimensionPixelOffset(
162                R.styleable.ContactListItemView_list_item_vertical_divider_margin, 0);
163        mPaddingTop = a.getDimensionPixelOffset(
164                R.styleable.ContactListItemView_list_item_padding_top, 0);
165        mPaddingBottom = a.getDimensionPixelOffset(
166                R.styleable.ContactListItemView_list_item_padding_bottom, 0);
167        mPaddingLeft = a.getDimensionPixelOffset(
168                R.styleable.ContactListItemView_list_item_padding_left, 0);
169        mPaddingRight = a.getDimensionPixelOffset(
170                R.styleable.ContactListItemView_list_item_padding_right, 0);
171        mGapBetweenImageAndText = a.getDimensionPixelOffset(
172                R.styleable.ContactListItemView_list_item_gap_between_image_and_text, 0);
173        mGapBetweenLabelAndData = a.getDimensionPixelOffset(
174                R.styleable.ContactListItemView_list_item_gap_between_label_and_data, 0);
175        mCallButtonPadding = a.getDimensionPixelOffset(
176                R.styleable.ContactListItemView_list_item_call_button_padding, 0);
177        mPresenceIconMargin = a.getDimensionPixelOffset(
178                R.styleable.ContactListItemView_list_item_presence_icon_margin, 0);
179        mDefaultPhotoViewSize = a.getDimensionPixelOffset(
180                R.styleable.ContactListItemView_list_item_photo_size, 0);
181        mPrefixHightlightColor = a.getColor(
182                R.styleable.ContactListItemView_list_item_prefix_highlight_color, Color.GREEN);
183
184        mHeaderTextIndent = a.getDimensionPixelOffset(
185                R.styleable.ContactListItemView_list_item_header_text_indent, 0);
186        mHeaderTextColor = a.getColor(
187                R.styleable.ContactListItemView_list_item_header_text_color, Color.BLACK);
188        mHeaderTextSize = a.getDimensionPixelSize(
189                R.styleable.ContactListItemView_list_item_header_text_size, 12);
190
191        a.recycle();
192
193        mHeaderBackgroundHeight = mHeaderBackgroundDrawable.getIntrinsicHeight();
194        mHorizontalDividerHeight = mHorizontalDividerDrawable.getIntrinsicHeight();
195
196        if (mActivatedBackgroundDrawable != null) {
197            mActivatedBackgroundDrawable.setCallback(this);
198        }
199    }
200
201    /**
202     * Installs a call button listener.
203     */
204    public void setOnCallButtonClickListener(OnClickListener callButtonClickListener) {
205        mCallButtonClickListener = callButtonClickListener;
206    }
207
208    public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) {
209        mTextWithHighlightingFactory = factory;
210    }
211
212    public void setUnknownNameText(CharSequence unknownNameText) {
213        mUnknownNameText = unknownNameText;
214    }
215
216    public void setQuickContactEnabled(boolean flag) {
217        mQuickContactEnabled = flag;
218    }
219
220    @Override
221    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
222        // We will match parent's width and wrap content vertically, but make sure
223        // height is no less than listPreferredItemHeight.
224        int width = resolveSize(0, widthMeasureSpec);
225        int height = 0;
226        int preferredHeight = mPreferredHeight;
227
228        mLine1Height = 0;
229        mLine2Height = 0;
230        mLine3Height = 0;
231        mLine4Height = 0;
232
233        // Obtain the natural dimensions of the name text (we only care about height)
234        if (isVisible(mNameTextView)) {
235            mNameTextView.measure(0, 0);
236            mLine1Height = mNameTextView.getMeasuredHeight();
237        }
238
239        if (isVisible(mPhoneticNameTextView)) {
240            mPhoneticNameTextView.measure(0, 0);
241            mLine2Height = mPhoneticNameTextView.getMeasuredHeight();
242        }
243
244        if (isVisible(mLabelView)) {
245            mLabelView.measure(0, 0);
246            mLine3Height = mLabelView.getMeasuredHeight();
247        }
248
249        if (isVisible(mDataView)) {
250            mDataView.measure(0, 0);
251            mLine3Height = Math.max(mLine3Height, mDataView.getMeasuredHeight());
252        }
253
254        if (isVisible(mSnippetView)) {
255            mSnippetView.measure(0, 0);
256            mLine4Height = mSnippetView.getMeasuredHeight();
257        }
258
259        height += mLine1Height + mLine2Height + mLine3Height + mLine4Height
260                + mPaddingTop + mPaddingBottom;
261
262        if (isVisible(mCallButton)) {
263            mCallButton.measure(0, 0);
264        }
265        if (isVisible(mPresenceIcon)) {
266            mPresenceIcon.measure(0, 0);
267        }
268
269        ensurePhotoViewSize();
270
271        height = Math.max(height, mPhotoViewHeight + mPaddingBottom + mPaddingTop);
272
273        if (mHorizontalDividerVisible) {
274            height += mHorizontalDividerHeight;
275            preferredHeight += mHorizontalDividerHeight;
276        }
277
278        height = Math.max(height, preferredHeight);
279
280        if (mHeaderVisible) {
281            mHeaderTextView.measure(
282                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
283                    MeasureSpec.makeMeasureSpec(mHeaderBackgroundHeight, MeasureSpec.EXACTLY));
284            height += mHeaderBackgroundHeight;
285        }
286
287        setMeasuredDimension(width, height);
288    }
289
290    @Override
291    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
292        int height = bottom - top;
293        int width = right - left;
294
295        // Determine the vertical bounds by laying out the header first.
296        int topBound = 0;
297        int bottomBound = height;
298
299        if (mHeaderVisible) {
300            mHeaderBackgroundDrawable.setBounds(
301                    0,
302                    0,
303                    width,
304                    mHeaderBackgroundHeight);
305            mHeaderTextView.layout(mHeaderTextIndent, 0, width, mHeaderBackgroundHeight);
306            topBound += mHeaderBackgroundHeight;
307        }
308
309        if (mHorizontalDividerVisible) {
310            mHorizontalDividerDrawable.setBounds(
311                    0,
312                    height - mHorizontalDividerHeight,
313                    width,
314                    height);
315            bottomBound -= mHorizontalDividerHeight;
316        }
317
318        mBoundsWithoutHeader.set(0, topBound, width, bottomBound);
319
320        if (mActivatedStateSupported) {
321            mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
322        }
323
324        topBound += mPaddingTop;
325        bottomBound -= mPaddingBottom;
326
327        // Positions of views on the left are fixed and so are those on the right side.
328        // The stretchable part of the layout is in the middle.  So, we will start off
329        // by laying out the left and right sides. Then we will allocate the remainder
330        // to the text fields in the middle.
331
332        int leftBound = layoutLeftSide(height, topBound, bottomBound, mPaddingLeft);
333        int rightBound = layoutRightSide(height, topBound, width);
334
335        // Text lines, centered vertically
336        rightBound -= mPaddingRight;
337
338        // Center text vertically
339        int totalTextHeight = mLine1Height + mLine2Height + mLine3Height + mLine4Height;
340        int textTopBound = (bottomBound + topBound - totalTextHeight) / 2;
341
342        if (isVisible(mNameTextView)) {
343            mNameTextView.layout(leftBound,
344                    textTopBound,
345                    rightBound,
346                    textTopBound + mLine1Height);
347        }
348
349        int dataLeftBound = leftBound;
350        if (isVisible(mPhoneticNameTextView)) {
351            mPhoneticNameTextView.layout(leftBound,
352                    textTopBound + mLine1Height,
353                    rightBound,
354                    textTopBound + mLine1Height + mLine2Height);
355        }
356
357        if (isVisible(mLabelView)) {
358            dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
359            mLabelView.layout(leftBound,
360                    textTopBound + mLine1Height + mLine2Height,
361                    dataLeftBound,
362                    textTopBound + mLine1Height + mLine2Height + mLine3Height);
363            dataLeftBound += mGapBetweenLabelAndData;
364        }
365
366        if (isVisible(mDataView)) {
367            mDataView.layout(dataLeftBound,
368                    textTopBound + mLine1Height + mLine2Height,
369                    rightBound,
370                    textTopBound + mLine1Height + mLine2Height + mLine3Height);
371        }
372
373        if (isVisible(mSnippetView)) {
374            mSnippetView.layout(leftBound,
375                    textTopBound + mLine1Height + mLine2Height + mLine3Height,
376                    rightBound,
377                    textTopBound + mLine1Height + mLine2Height + mLine3Height + mLine4Height);
378        }
379    }
380
381    /**
382     * Performs layout of the left side of the view
383     *
384     * @return new left boundary
385     */
386    protected int layoutLeftSide(int height, int topBound, int bottomBound, int leftBound) {
387        View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
388        if (photoView != null) {
389            // Center the photo vertically
390            int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
391            photoView.layout(
392                    leftBound,
393                    photoTop,
394                    leftBound + mPhotoViewWidth,
395                    photoTop + mPhotoViewHeight);
396            leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
397        }
398        return leftBound;
399    }
400
401    /**
402     * Performs layout of the right side of the view
403     *
404     * @return new right boundary
405     */
406    protected int layoutRightSide(int height, int topBound, int rightBound) {
407        if (isVisible(mCallButton)) {
408            int buttonWidth = mCallButton.getMeasuredWidth();
409            rightBound -= buttonWidth;
410            mCallButton.layout(
411                    rightBound,
412                    topBound,
413                    rightBound + buttonWidth,
414                    height - mHorizontalDividerHeight);
415            mVerticalDividerVisible = true;
416            ensureVerticalDivider();
417            rightBound -= mVerticalDividerWidth;
418            mVerticalDividerDrawable.setBounds(
419                    rightBound,
420                    topBound + mVerticalDividerMargin,
421                    rightBound + mVerticalDividerWidth,
422                    height - mVerticalDividerMargin);
423        } else {
424            mVerticalDividerVisible = false;
425        }
426
427        if (isVisible(mPresenceIcon)) {
428            int iconWidth = mPresenceIcon.getMeasuredWidth();
429            rightBound -= mPresenceIconMargin + iconWidth;
430            mPresenceIcon.layout(
431                    rightBound,
432                    topBound,
433                    rightBound + iconWidth,
434                    height);
435        }
436        return rightBound;
437    }
438
439    @Override
440    public void adjustListItemSelectionBounds(Rect bounds) {
441        bounds.top += mBoundsWithoutHeader.top;
442        bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
443    }
444
445    protected boolean isVisible(View view) {
446        return view != null && view.getVisibility() == View.VISIBLE;
447    }
448
449    /**
450     * Loads the drawable for the vertical divider if it has not yet been loaded.
451     */
452    private void ensureVerticalDivider() {
453        if (mVerticalDividerDrawable == null) {
454            mVerticalDividerDrawable = mContext.getResources().getDrawable(
455                    R.drawable.divider_vertical_dark);
456            mVerticalDividerWidth = mVerticalDividerDrawable.getIntrinsicWidth();
457        }
458    }
459
460    /**
461     * Extracts width and height from the style
462     */
463    private void ensurePhotoViewSize() {
464        if (mPhotoViewWidth == 0 && mPhotoViewHeight == 0) {
465            if (mQuickContactEnabled) {
466                TypedArray a = mContext.obtainStyledAttributes(null,
467                        com.android.internal.R.styleable.ViewGroup_Layout,
468                        QUICK_CONTACT_BADGE_STYLE, 0);
469                mPhotoViewWidth = a.getLayoutDimension(
470                        android.R.styleable.ViewGroup_Layout_layout_width,
471                        ViewGroup.LayoutParams.WRAP_CONTENT);
472                mPhotoViewHeight = a.getLayoutDimension(
473                        android.R.styleable.ViewGroup_Layout_layout_height,
474                        ViewGroup.LayoutParams.WRAP_CONTENT);
475                a.recycle();
476            } else {
477                mPhotoViewWidth = mPhotoViewHeight = mDefaultPhotoViewSize;
478            }
479        }
480    }
481
482    @Override
483    protected void drawableStateChanged() {
484        super.drawableStateChanged();
485        if (mActivatedStateSupported) {
486            mActivatedBackgroundDrawable.setState(getDrawableState());
487        }
488    }
489
490    @Override
491    protected boolean verifyDrawable(Drawable who) {
492        return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
493    }
494
495    @Override
496    public void jumpDrawablesToCurrentState() {
497        super.jumpDrawablesToCurrentState();
498        if (mActivatedStateSupported) {
499            mActivatedBackgroundDrawable.jumpToCurrentState();
500        }
501    }
502
503    @Override
504    public void dispatchDraw(Canvas canvas) {
505        if (mActivatedStateSupported) {
506            mActivatedBackgroundDrawable.draw(canvas);
507        }
508        if (mHeaderVisible) {
509            mHeaderBackgroundDrawable.draw(canvas);
510        }
511        if (mHorizontalDividerVisible) {
512            mHorizontalDividerDrawable.draw(canvas);
513        }
514        if (mVerticalDividerVisible) {
515            mVerticalDividerDrawable.draw(canvas);
516        }
517
518        super.dispatchDraw(canvas);
519    }
520
521    /**
522     * Sets the flag that determines whether a divider should drawn at the bottom
523     * of the view.
524     */
525    public void setDividerVisible(boolean visible) {
526        mHorizontalDividerVisible = visible;
527    }
528
529    /**
530     * Sets section header or makes it invisible if the title is null.
531     */
532    public void setSectionHeader(String title) {
533        if (!TextUtils.isEmpty(title)) {
534            if (mHeaderTextView == null) {
535                mHeaderTextView = new TextView(mContext);
536                mHeaderTextView.setTextColor(mHeaderTextColor);
537                mHeaderTextView.setTextSize(mHeaderTextSize);
538                mHeaderTextView.setTypeface(mHeaderTextView.getTypeface(), Typeface.BOLD);
539                mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL);
540                addView(mHeaderTextView);
541            }
542            mHeaderTextView.setText(title);
543            mHeaderTextView.setVisibility(View.VISIBLE);
544            mHeaderVisible = true;
545        } else {
546            if (mHeaderTextView != null) {
547                mHeaderTextView.setVisibility(View.GONE);
548            }
549            mHeaderVisible = false;
550        }
551    }
552
553    /**
554     * Returns the quick contact badge, creating it if necessary.
555     */
556    public QuickContactBadge getQuickContact() {
557        if (!mQuickContactEnabled) {
558            throw new IllegalStateException("QuickContact is disabled for this view");
559        }
560        if (mQuickContact == null) {
561            mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE);
562            mQuickContact.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE });
563            addView(mQuickContact);
564        }
565        return mQuickContact;
566    }
567
568    /**
569     * Returns the photo view, creating it if necessary.
570     */
571    public ImageView getPhotoView() {
572        if (mPhotoView == null) {
573            if (mQuickContactEnabled) {
574                mPhotoView = new ImageView(mContext, null, QUICK_CONTACT_BADGE_STYLE);
575            } else {
576                mPhotoView = new ImageView(mContext);
577            }
578            // Quick contact style used above will set a background - remove it
579            mPhotoView.setBackgroundDrawable(null);
580            addView(mPhotoView);
581        }
582        return mPhotoView;
583    }
584
585    /**
586     * Removes the photo view.  Should not be needed once we start handling different
587     * types of views as different types of views from the List's perspective.
588     */
589    public void removePhotoView() {
590        if (mPhotoView != null) {
591            removeView(mPhotoView);
592            mPhotoView = null;
593        }
594        if (mQuickContact != null) {
595            removeView(mQuickContact);
596            mQuickContact = null;
597        }
598    }
599
600    /**
601     * Sets a word prefix that will be highlighted if encountered in fields like
602     * name and search snippet.
603     * <p>
604     * NOTE: must be all upper-case
605     */
606    public void setHighlightedPrefix(char[] upperCasePrefix) {
607        mHighlightedPrefix = upperCasePrefix;
608    }
609
610    /**
611     * Returns the text view for the contact name, creating it if necessary.
612     */
613    public TextView getNameTextView() {
614        if (mNameTextView == null) {
615            mNameTextView = new TextView(mContext);
616            mNameTextView.setSingleLine(true);
617            mNameTextView.setEllipsize(getTextEllipsis());
618            mNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
619            mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
620            addView(mNameTextView);
621        }
622        return mNameTextView;
623    }
624
625    /**
626     * Adds a call button using the supplied arguments as an id and tag.
627     */
628    public void showCallButton(int id, int tag) {
629        if (mCallButton == null) {
630            mCallButton = new DontPressWithParentImageView(mContext, null);
631            mCallButton.setId(id);
632            mCallButton.setOnClickListener(mCallButtonClickListener);
633            mCallButton.setBackgroundResource(R.drawable.call_background);
634            mCallButton.setImageResource(android.R.drawable.sym_action_call);
635            mCallButton.setPadding(mCallButtonPadding, 0, mCallButtonPadding, 0);
636            mCallButton.setScaleType(ScaleType.CENTER);
637            addView(mCallButton);
638        }
639
640        mCallButton.setTag(tag);
641        mCallButton.setVisibility(View.VISIBLE);
642    }
643
644    public void hideCallButton() {
645        if (mCallButton != null) {
646            mCallButton.setVisibility(View.GONE);
647        }
648    }
649
650    /**
651     * Adds or updates a text view for the phonetic name.
652     */
653    public void setPhoneticName(char[] text, int size) {
654        if (text == null || size == 0) {
655            if (mPhoneticNameTextView != null) {
656                mPhoneticNameTextView.setVisibility(View.GONE);
657            }
658        } else {
659            getPhoneticNameTextView();
660            mPhoneticNameTextView.setText(text, 0, size);
661            mPhoneticNameTextView.setVisibility(VISIBLE);
662        }
663    }
664
665    /**
666     * Returns the text view for the phonetic name, creating it if necessary.
667     */
668    public TextView getPhoneticNameTextView() {
669        if (mPhoneticNameTextView == null) {
670            mPhoneticNameTextView = new TextView(mContext);
671            mPhoneticNameTextView.setSingleLine(true);
672            mPhoneticNameTextView.setEllipsize(getTextEllipsis());
673            mPhoneticNameTextView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
674            mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
675            addView(mPhoneticNameTextView);
676        }
677        return mPhoneticNameTextView;
678    }
679
680    /**
681     * Adds or updates a text view for the data label.
682     */
683    public void setLabel(CharSequence text) {
684        if (TextUtils.isEmpty(text)) {
685            if (mLabelView != null) {
686                mLabelView.setVisibility(View.GONE);
687            }
688        } else {
689            getLabelView();
690            mLabelView.setText(text);
691            mLabelView.setVisibility(VISIBLE);
692        }
693    }
694
695    /**
696     * Adds or updates a text view for the data label.
697     */
698    public void setLabel(char[] text, int size) {
699        if (text == null || size == 0) {
700            if (mLabelView != null) {
701                mLabelView.setVisibility(View.GONE);
702            }
703        } else {
704            getLabelView();
705            mLabelView.setText(text, 0, size);
706            mLabelView.setVisibility(VISIBLE);
707        }
708    }
709
710    /**
711     * Returns the text view for the data label, creating it if necessary.
712     */
713    public TextView getLabelView() {
714        if (mLabelView == null) {
715            mLabelView = new TextView(mContext);
716            mLabelView.setSingleLine(true);
717            mLabelView.setEllipsize(getTextEllipsis());
718            mLabelView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
719            mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
720            addView(mLabelView);
721        }
722        return mLabelView;
723    }
724
725    /**
726     * Adds or updates a text view for the data element.
727     */
728    public void setData(char[] text, int size) {
729        if (text == null || size == 0) {
730            if (mDataView != null) {
731                mDataView.setVisibility(View.GONE);
732            }
733            return;
734        } else {
735            getDataView();
736            mDataView.setText(text, 0, size);
737            mDataView.setVisibility(VISIBLE);
738        }
739    }
740
741    /**
742     * Returns the text view for the data text, creating it if necessary.
743     */
744    public TextView getDataView() {
745        if (mDataView == null) {
746            mDataView = new TextView(mContext);
747            mDataView.setSingleLine(true);
748            mDataView.setEllipsize(getTextEllipsis());
749            mDataView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
750            addView(mDataView);
751        }
752        return mDataView;
753    }
754
755    /**
756     * Adds or updates a text view for the search snippet.
757     */
758    public void setSnippet(String text) {
759        if (TextUtils.isEmpty(text)) {
760            if (mSnippetView != null) {
761                mSnippetView.setVisibility(View.GONE);
762            }
763        } else {
764            getSnippetView();
765            setTextWithPrefixHighlighting(mSnippetView, text);
766            mSnippetView.setVisibility(VISIBLE);
767        }
768    }
769
770    /**
771     * Returns the text view for the search snippet, creating it if necessary.
772     */
773    public TextView getSnippetView() {
774        if (mSnippetView == null) {
775            mSnippetView = new TextView(mContext);
776            mSnippetView.setSingleLine(true);
777            mSnippetView.setEllipsize(getTextEllipsis());
778            mSnippetView.setTextAppearance(mContext, android.R.style.TextAppearance_Small);
779            mSnippetView.setTypeface(mSnippetView.getTypeface(), Typeface.BOLD);
780            addView(mSnippetView);
781        }
782        return mSnippetView;
783    }
784
785    /**
786     * Adds or updates the presence icon view.
787     */
788    public void setPresence(Drawable icon) {
789        if (icon != null) {
790            if (mPresenceIcon == null) {
791                mPresenceIcon = new ImageView(mContext);
792                addView(mPresenceIcon);
793            }
794            mPresenceIcon.setImageDrawable(icon);
795            mPresenceIcon.setScaleType(ScaleType.CENTER);
796            mPresenceIcon.setVisibility(View.VISIBLE);
797        } else {
798            if (mPresenceIcon != null) {
799                mPresenceIcon.setVisibility(View.GONE);
800            }
801        }
802    }
803
804    private TruncateAt getTextEllipsis() {
805        return mActivatedStateSupported ? TruncateAt.START : TruncateAt.MARQUEE;
806    }
807
808    public void showDisplayName(Cursor cursor, int nameColumnIndex, boolean highlightingEnabled,
809            int alternativeNameColumnIndex) {
810        cursor.copyStringToBuffer(nameColumnIndex, mNameBuffer);
811        TextView nameView = getNameTextView();
812        int size = mNameBuffer.sizeCopied;
813        if (size != 0) {
814            if (mHighlightedPrefix != null) {
815                setTextWithPrefixHighlighting(nameView, mNameBuffer);
816            } else if (highlightingEnabled) {
817                if (mTextWithHighlighting == null) {
818                    mTextWithHighlighting =
819                            mTextWithHighlightingFactory.createTextWithHighlighting();
820                }
821                cursor.copyStringToBuffer(alternativeNameColumnIndex, mHighlightedTextBuffer);
822                mTextWithHighlighting.setText(mNameBuffer, mHighlightedTextBuffer);
823                nameView.setText(mTextWithHighlighting);
824            } else {
825                nameView.setText(mNameBuffer.data, 0, size);
826            }
827        } else {
828            nameView.setText(mUnknownNameText);
829        }
830    }
831
832    public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
833        cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
834        int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
835        if (phoneticNameSize != 0) {
836            setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
837        } else {
838            setPhoneticName(null, 0);
839        }
840    }
841
842    /**
843     * Sets the proper icon (star or presence or nothing)
844     */
845    public void showPresence(Cursor cursor, int presenceColumnIndex, int capabilityColumnIndex) {
846        Drawable icon = null;
847        if (!cursor.isNull(presenceColumnIndex)) {
848            int status = cursor.getInt(presenceColumnIndex);
849            int chatCapability = 0;
850            if (capabilityColumnIndex != 0 && !cursor.isNull(presenceColumnIndex)) {
851                chatCapability = cursor.getInt(capabilityColumnIndex);
852            }
853            icon = ContactPresenceIconUtil.getChatCapabilityIcon(
854                    getContext(), status, chatCapability);
855        }
856        setPresence(icon);
857    }
858
859    /**
860     * Shows search snippet.
861     */
862    public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
863        if (cursor.getColumnCount() <= summarySnippetColumnIndex) {
864            setSnippet(null);
865            return;
866        }
867
868        String snippet = cursor.getString(summarySnippetColumnIndex);
869        if (snippet != null) {
870            int from = 0;
871            int to = snippet.length();
872            int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
873            if (start != -1) {
874                int firstNl = snippet.lastIndexOf('\n', start);
875                if (firstNl != -1) {
876                    from = firstNl + 1;
877                }
878            }
879            int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
880            if (end != -1) {
881                int lastNl = snippet.indexOf('\n', end);
882                if (lastNl != -1) {
883                    to = lastNl;
884                }
885            }
886
887            StringBuilder sb = new StringBuilder();
888            for (int i = from; i < to; i++) {
889                char c = snippet.charAt(i);
890                if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
891                        c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
892                    sb.append(c);
893                }
894            }
895            snippet = sb.toString();
896        }
897        setSnippet(snippet);
898    }
899
900    /**
901     * Shows data element (e.g. phone number).
902     */
903    public void showData(Cursor cursor, int dataColumnIndex) {
904        cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
905        setData(mDataBuffer.data, mDataBuffer.sizeCopied);
906    }
907
908    public void setActivatedStateSupported(boolean flag) {
909        this.mActivatedStateSupported = flag;
910    }
911
912    /**
913     * Sets text on the given text view, highlighting the word that matches
914     * the given prefix (see {@link #setHighlightedPrefix}).
915     */
916    private void setTextWithPrefixHighlighting(TextView textView, String text) {
917        mHighlightedTextBuffer.sizeCopied =
918                Math.min(text.length(), mHighlightedTextBuffer.data.length);
919        text.getChars(0, mHighlightedTextBuffer.sizeCopied, mHighlightedTextBuffer.data, 0);
920        setTextWithPrefixHighlighting(textView, mHighlightedTextBuffer);
921    }
922
923    /**
924     * Sets text on the given text view, highlighting the word that matches
925     * the given prefix (see {@link #setHighlightedPrefix}).
926     */
927    private void setTextWithPrefixHighlighting(TextView textView, CharArrayBuffer text) {
928        int index = indexOfWordPrefix(text, mHighlightedPrefix);
929        if (index != -1) {
930            if (mPrefixColorSpan == null) {
931                mPrefixColorSpan = new ForegroundColorSpan(mPrefixHightlightColor);
932            }
933
934            String string = new String(text.data, 0, text.sizeCopied);
935            SpannableString name = new SpannableString(string);
936            name.setSpan(mPrefixColorSpan, index, index + mHighlightedPrefix.length, 0 /* flags */);
937            textView.setText(name);
938        } else {
939            textView.setText(text.data, 0, text.sizeCopied);
940        }
941    }
942
943    /**
944     * Finds the index of the word that starts with the given prefix.  If not found,
945     * returns -1.
946     */
947    private int indexOfWordPrefix(CharArrayBuffer buffer, char[] prefix) {
948        if (prefix == null || prefix.length == 0) {
949            return -1;
950        }
951
952        char[] string1 = buffer.data;
953        int count1 = buffer.sizeCopied;
954        int count2 = prefix.length;
955
956        int size = count2;
957        int i = 0;
958        while (i < count1) {
959
960            // Skip non-word characters
961            while (i < count1 && !Character.isLetterOrDigit(string1[i])) {
962                i++;
963            }
964
965            if (i + size > count1) {
966                return -1;
967            }
968
969            // Compare the prefixes
970            int j;
971            for (j = 0; j < size; j++) {
972                if (Character.toUpperCase(string1[i+j]) != prefix[j]) {
973                    break;
974                }
975            }
976            if (j == size) {
977                return i;
978            }
979
980            // Skip this word
981            while (i < count1 && Character.isLetterOrDigit(string1[i])) {
982                i++;
983            }
984        }
985
986        return -1;
987    }
988
989    @Override
990    public void requestLayout() {
991        // We will assume that once measured this will not need to resize
992        // itself, so there is no need to pass the layout request to the parent
993        // view (ListView).
994        forceLayout();
995    }
996}
997