blob: e3880acc6a8c1afec34a18f3327cd61fa742bf10 [file] [log] [blame]
Chiao Cheng89437e82012-11-01 13:41:51 -07001/*
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.common.list;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.TypedArray;
22import android.database.CharArrayBuffer;
23import android.database.Cursor;
24import android.graphics.Canvas;
Andrew Lee7b0cbeb2014-05-27 17:54:36 -070025import android.graphics.Color;
Chiao Cheng89437e82012-11-01 13:41:51 -070026import android.graphics.Rect;
27import android.graphics.Typeface;
28import android.graphics.drawable.Drawable;
29import android.os.Bundle;
30import android.provider.ContactsContract;
31import android.provider.ContactsContract.Contacts;
Walter Jang54564402016-01-18 11:56:19 -080032import android.provider.ContactsContract.SearchSnippets;
Wenyi Wang34d81ed2015-11-19 15:06:56 -080033import android.support.v4.content.ContextCompat;
Wenyi Wang54ea4b12015-12-16 14:18:59 -080034import android.support.v4.graphics.drawable.DrawableCompat;
Wenyi Wange4e359f2016-04-02 14:30:02 -070035import android.support.v7.widget.AppCompatCheckBox;
Walter Jang379ff852016-05-22 12:17:39 -070036import android.support.v7.widget.AppCompatImageButton;
Chiao Cheng89437e82012-11-01 13:41:51 -070037import android.text.Spannable;
38import android.text.SpannableString;
39import android.text.TextUtils;
40import android.text.TextUtils.TruncateAt;
41import android.util.AttributeSet;
Sai Cheemalapati7c776a52014-05-27 16:37:15 -070042import android.util.TypedValue;
Chiao Cheng89437e82012-11-01 13:41:51 -070043import android.view.Gravity;
Yorke Lee1c171fa2013-09-06 13:53:49 -070044import android.view.MotionEvent;
Chiao Cheng89437e82012-11-01 13:41:51 -070045import android.view.View;
46import android.view.ViewGroup;
47import android.widget.AbsListView.SelectionBoundsAdjuster;
48import android.widget.ImageView;
49import android.widget.ImageView.ScaleType;
50import android.widget.QuickContactBadge;
51import android.widget.TextView;
52
53import com.android.contacts.common.ContactPresenceIconUtil;
54import com.android.contacts.common.ContactStatusUtil;
55import com.android.contacts.common.R;
Wenyi Wang54ea4b12015-12-16 14:18:59 -080056import com.android.contacts.common.compat.CompatUtils;
Wenyi Wang40346982015-11-18 14:58:42 -080057import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
Christine Chen3efbe592013-07-08 18:05:03 -070058import com.android.contacts.common.format.TextHighlighter;
Walter Jang1a21fe52014-10-15 18:32:44 -070059import com.android.contacts.common.util.ContactDisplayUtils;
Chiao Chengecba27e2012-12-27 17:14:53 -080060import com.android.contacts.common.util.SearchUtil;
Yorke Lee50a89a52013-11-04 14:44:30 -080061import com.android.contacts.common.util.ViewUtil;
62
Chiao Cheng901c5e52012-12-19 16:06:37 -080063import com.google.common.collect.Lists;
64
65import java.util.ArrayList;
66import java.util.List;
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -070067import java.util.Locale;
Chiao Cheng901c5e52012-12-19 16:06:37 -080068import java.util.regex.Matcher;
69import java.util.regex.Pattern;
Chiao Cheng89437e82012-11-01 13:41:51 -070070
71/**
72 * A custom view for an item in the contact list.
73 * The view contains the contact's photo, a set of text views (for name, status, etc...) and
74 * icons for presence and call.
75 * The view uses no XML file for layout and all the measurements and layouts are done
76 * in the onMeasure and onLayout methods.
77 *
78 * The layout puts the contact's photo on the right side of the view, the call icon (if present)
79 * to the left of the photo, the text lines are aligned to the left and the presence icon (if
80 * present) is set to the left of the status line.
81 *
82 * The layout also supports a header (used as a header of a group of contacts) that is above the
83 * contact's data and a divider between contact view.
84 */
85
86public class ContactListItemView extends ViewGroup
87 implements SelectionBoundsAdjuster {
88
Tyler Gunn001d9742015-12-18 13:57:02 -080089 private static final String TAG = "ContactListItemView";
90
Chiao Cheng89437e82012-11-01 13:41:51 -070091 // Style values for layout and appearance
92 // The initialized values are defaults if none is provided through xml.
93 private int mPreferredHeight = 0;
94 private int mGapBetweenImageAndText = 0;
Wenyi Wangb8b2fa32016-05-23 15:52:23 -070095 private int mGapBetweenIndexerAndImage = 0;
Chiao Cheng89437e82012-11-01 13:41:51 -070096 private int mGapBetweenLabelAndData = 0;
97 private int mPresenceIconMargin = 4;
98 private int mPresenceIconSize = 16;
Chiao Cheng89437e82012-11-01 13:41:51 -070099 private int mTextIndent = 0;
Andrew Leeba448962014-05-29 16:20:19 -0700100 private int mTextOffsetTop;
Sai Cheemalapati7c776a52014-05-27 16:37:15 -0700101 private int mNameTextViewTextSize;
Andrew Lee020ba622014-04-25 15:26:35 -0700102 private int mHeaderWidth;
Chiao Cheng89437e82012-11-01 13:41:51 -0700103 private Drawable mActivatedBackgroundDrawable;
Tyler Gunn001d9742015-12-18 13:57:02 -0800104 private int mVideoCallIconSize = 32;
105 private int mVideoCallIconMargin = 16;
Chiao Cheng89437e82012-11-01 13:41:51 -0700106
Sai Cheemalapatid814b032014-06-30 14:46:25 -0700107 // Set in onLayout. Represent left and right position of the View on the screen.
108 private int mLeftOffset;
109 private int mRightOffset;
110
Chiao Cheng89437e82012-11-01 13:41:51 -0700111 /**
112 * Used with {@link #mLabelView}, specifying the width ratio between label and data.
113 */
114 private int mLabelViewWidthWeight = 3;
115 /**
116 * Used with {@link #mDataView}, specifying the width ratio between label and data.
117 */
118 private int mDataViewWidthWeight = 5;
119
Christine Chenccba9502013-07-12 12:04:54 -0700120 protected static class HighlightSequence {
121 private final int start;
122 private final int end;
123
124 HighlightSequence(int start, int end) {
125 this.start = start;
126 this.end = end;
127 }
128 }
129
130 private ArrayList<HighlightSequence> mNameHighlightSequence;
131 private ArrayList<HighlightSequence> mNumberHighlightSequence;
Christine Chen3efbe592013-07-08 18:05:03 -0700132
133 // Highlighting prefix for names.
134 private String mHighlightedPrefix;
135
Chiao Cheng89437e82012-11-01 13:41:51 -0700136 /**
Tyler Gunn001d9742015-12-18 13:57:02 -0800137 * Used to notify listeners when a video call icon is clicked.
138 */
139 private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener;
140
141 /**
142 * Indicates whether to show the "video call" icon, used to initiate a video call.
143 */
144 private boolean mShowVideoCallIcon = false;
145
146 /**
147 * Indicates whether the view should leave room for the "video call" icon.
148 */
149 private boolean mSupportVideoCallIcon = false;
150
151 /**
Chiao Cheng89437e82012-11-01 13:41:51 -0700152 * Where to put contact photo. This affects the other Views' layout or look-and-feel.
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700153 *
154 * TODO: replace enum with int constants
Chiao Cheng89437e82012-11-01 13:41:51 -0700155 */
156 public enum PhotoPosition {
157 LEFT,
158 RIGHT
159 }
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700160
161 static public final PhotoPosition getDefaultPhotoPosition(boolean opposite) {
162 final Locale locale = Locale.getDefault();
163 final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
164 switch (layoutDirection) {
165 case View.LAYOUT_DIRECTION_RTL:
Andrew Leea88f4b22014-04-25 13:24:03 -0700166 return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT);
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700167 case View.LAYOUT_DIRECTION_LTR:
168 default:
Andrew Leea88f4b22014-04-25 13:24:03 -0700169 return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT);
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700170 }
171 }
172
173 private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */);
Chiao Cheng89437e82012-11-01 13:41:51 -0700174
175 // Header layout data
Chiao Cheng89437e82012-11-01 13:41:51 -0700176 private TextView mHeaderTextView;
Andrew Lee020ba622014-04-25 15:26:35 -0700177 private boolean mIsSectionHeaderEnabled;
Chiao Cheng89437e82012-11-01 13:41:51 -0700178
179 // The views inside the contact view
180 private boolean mQuickContactEnabled = true;
181 private QuickContactBadge mQuickContact;
182 private ImageView mPhotoView;
183 private TextView mNameTextView;
184 private TextView mPhoneticNameTextView;
185 private TextView mLabelView;
186 private TextView mDataView;
187 private TextView mSnippetView;
188 private TextView mStatusView;
Chiao Cheng89437e82012-11-01 13:41:51 -0700189 private ImageView mPresenceIcon;
Wenyi Wange4e359f2016-04-02 14:30:02 -0700190 private AppCompatCheckBox mCheckBox;
Walter Jang379ff852016-05-22 12:17:39 -0700191 private AppCompatImageButton mDeleteImageButton;
Tyler Gunn001d9742015-12-18 13:57:02 -0800192 private ImageView mVideoCallIcon;
Victor Chang3e21eef2015-12-24 13:17:42 +0800193 private ImageView mWorkProfileIcon;
Chiao Cheng89437e82012-11-01 13:41:51 -0700194
195 private ColorStateList mSecondaryTextColor;
196
Chiao Cheng89437e82012-11-01 13:41:51 -0700197 private int mDefaultPhotoViewSize = 0;
198 /**
199 * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
200 * to align other data in this View.
201 */
202 private int mPhotoViewWidth;
203 /**
204 * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
205 */
206 private int mPhotoViewHeight;
207
208 /**
209 * Only effective when {@link #mPhotoView} is null.
210 * When true all the Views on the right side of the photo should have horizontal padding on
211 * those left assuming there is a photo.
212 */
213 private boolean mKeepHorizontalPaddingForPhotoView;
214 /**
215 * Only effective when {@link #mPhotoView} is null.
216 */
217 private boolean mKeepVerticalPaddingForPhotoView;
218
219 /**
220 * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
221 * False indicates those values should be updated before being used in position calculation.
222 */
223 private boolean mPhotoViewWidthAndHeightAreReady = false;
224
225 private int mNameTextViewHeight;
Andrew Lee7b0cbeb2014-05-27 17:54:36 -0700226 private int mNameTextViewTextColor = Color.BLACK;
Chiao Cheng89437e82012-11-01 13:41:51 -0700227 private int mPhoneticNameTextViewHeight;
228 private int mLabelViewHeight;
229 private int mDataViewHeight;
230 private int mSnippetTextViewHeight;
231 private int mStatusTextViewHeight;
Brian Attwell207a8772015-02-27 16:35:00 -0800232 private int mCheckBoxHeight;
233 private int mCheckBoxWidth;
Walter Jang379ff852016-05-22 12:17:39 -0700234 private int mDeleteImageButtonHeight;
235 private int mDeleteImageButtonWidth;
Chiao Cheng89437e82012-11-01 13:41:51 -0700236
237 // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
238 // same row.
239 private int mLabelAndDataViewMaxHeight;
240
241 // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
242 // more efficient for each case or in general, and simplify the whole implementation.
243 // Note: if we're sure MARQUEE will be used every time, there's no reason to use
244 // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
245 // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
246 // TextView without any modification.
247 private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
248 private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
249
250 private boolean mActivatedStateSupported;
Andrew Lee4683e542014-06-09 16:24:10 -0700251 private boolean mAdjustSelectionBoundsEnabled = true;
Chiao Cheng89437e82012-11-01 13:41:51 -0700252
253 private Rect mBoundsWithoutHeader = new Rect();
254
255 /** A helper used to highlight a prefix in a text field. */
Christine Chen3efbe592013-07-08 18:05:03 -0700256 private final TextHighlighter mTextHighlighter;
Chiao Cheng89437e82012-11-01 13:41:51 -0700257 private CharSequence mUnknownNameText;
Tyler Gunn001d9742015-12-18 13:57:02 -0800258 private int mPosition;
Chiao Cheng89437e82012-11-01 13:41:51 -0700259
260 public ContactListItemView(Context context) {
261 super(context);
Chiao Cheng89437e82012-11-01 13:41:51 -0700262
Yorke Lee8b7f84a2013-09-12 12:38:41 -0700263 mTextHighlighter = new TextHighlighter(Typeface.BOLD);
Yorke Lee426e5d42014-03-18 13:07:00 -0700264 mNameHighlightSequence = new ArrayList<HighlightSequence>();
265 mNumberHighlightSequence = new ArrayList<HighlightSequence>();
Chiao Cheng89437e82012-11-01 13:41:51 -0700266 }
267
Tyler Gunn001d9742015-12-18 13:57:02 -0800268 public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) {
269 this(context, attrs);
270
271 mSupportVideoCallIcon = supportVideoCallIcon;
272 }
273
Chiao Cheng89437e82012-11-01 13:41:51 -0700274 public ContactListItemView(Context context, AttributeSet attrs) {
275 super(context, attrs);
Chiao Cheng89437e82012-11-01 13:41:51 -0700276
Yorke Leea675e3a2015-10-26 19:43:22 -0700277 TypedArray a;
Chiao Cheng89437e82012-11-01 13:41:51 -0700278
Yorke Leea675e3a2015-10-26 19:43:22 -0700279 if (R.styleable.ContactListItemView != null) {
280 // Read all style values
281 a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
282 mPreferredHeight = a.getDimensionPixelSize(
283 R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
284 mActivatedBackgroundDrawable = a.getDrawable(
285 R.styleable.ContactListItemView_activated_background);
Chiao Cheng89437e82012-11-01 13:41:51 -0700286
Yorke Leea675e3a2015-10-26 19:43:22 -0700287 mGapBetweenImageAndText = a.getDimensionPixelOffset(
288 R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
289 mGapBetweenImageAndText);
Wenyi Wangb8b2fa32016-05-23 15:52:23 -0700290 mGapBetweenIndexerAndImage = a.getDimensionPixelOffset(
291 R.styleable.ContactListItemView_list_item_gap_between_indexer_and_image,
292 mGapBetweenIndexerAndImage);
Yorke Leea675e3a2015-10-26 19:43:22 -0700293 mGapBetweenLabelAndData = a.getDimensionPixelOffset(
294 R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
295 mGapBetweenLabelAndData);
296 mPresenceIconMargin = a.getDimensionPixelOffset(
297 R.styleable.ContactListItemView_list_item_presence_icon_margin,
298 mPresenceIconMargin);
299 mPresenceIconSize = a.getDimensionPixelOffset(
300 R.styleable.ContactListItemView_list_item_presence_icon_size,
301 mPresenceIconSize);
302 mDefaultPhotoViewSize = a.getDimensionPixelOffset(
303 R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
304 mTextIndent = a.getDimensionPixelOffset(
305 R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
306 mTextOffsetTop = a.getDimensionPixelOffset(
307 R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop);
308 mDataViewWidthWeight = a.getInteger(
309 R.styleable.ContactListItemView_list_item_data_width_weight,
310 mDataViewWidthWeight);
311 mLabelViewWidthWeight = a.getInteger(
312 R.styleable.ContactListItemView_list_item_label_width_weight,
313 mLabelViewWidthWeight);
314 mNameTextViewTextColor = a.getColor(
315 R.styleable.ContactListItemView_list_item_name_text_color,
316 mNameTextViewTextColor);
317 mNameTextViewTextSize = (int) a.getDimension(
318 R.styleable.ContactListItemView_list_item_name_text_size,
319 (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
Tyler Gunn001d9742015-12-18 13:57:02 -0800320 mVideoCallIconSize = a.getDimensionPixelOffset(
321 R.styleable.ContactListItemView_list_item_video_call_icon_size,
322 mVideoCallIconSize);
323 mVideoCallIconMargin = a.getDimensionPixelOffset(
324 R.styleable.ContactListItemView_list_item_video_call_icon_margin,
325 mVideoCallIconMargin);
326
Yorke Leea675e3a2015-10-26 19:43:22 -0700327
328 setPaddingRelative(
329 a.getDimensionPixelOffset(
330 R.styleable.ContactListItemView_list_item_padding_left, 0),
331 a.getDimensionPixelOffset(
332 R.styleable.ContactListItemView_list_item_padding_top, 0),
333 a.getDimensionPixelOffset(
334 R.styleable.ContactListItemView_list_item_padding_right, 0),
335 a.getDimensionPixelOffset(
336 R.styleable.ContactListItemView_list_item_padding_bottom, 0));
337
338 a.recycle();
339 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700340
Yorke Lee8b7f84a2013-09-12 12:38:41 -0700341 mTextHighlighter = new TextHighlighter(Typeface.BOLD);
342
Yorke Leea675e3a2015-10-26 19:43:22 -0700343 if (R.styleable.Theme != null) {
344 a = getContext().obtainStyledAttributes(R.styleable.Theme);
345 mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary);
346 a.recycle();
347 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700348
Andrew Lee020ba622014-04-25 15:26:35 -0700349 mHeaderWidth =
350 getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
351
Chiao Cheng89437e82012-11-01 13:41:51 -0700352 if (mActivatedBackgroundDrawable != null) {
353 mActivatedBackgroundDrawable.setCallback(this);
354 }
Christine Chenccba9502013-07-12 12:04:54 -0700355
356 mNameHighlightSequence = new ArrayList<HighlightSequence>();
357 mNumberHighlightSequence = new ArrayList<HighlightSequence>();
Yorke Lee525f5132014-07-30 18:59:28 -0700358
359 setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
Chiao Cheng89437e82012-11-01 13:41:51 -0700360 }
361
362 public void setUnknownNameText(CharSequence unknownNameText) {
363 mUnknownNameText = unknownNameText;
364 }
365
366 public void setQuickContactEnabled(boolean flag) {
367 mQuickContactEnabled = flag;
368 }
369
Tyler Gunn001d9742015-12-18 13:57:02 -0800370 /**
371 * Sets whether the video calling icon is shown. For the video calling icon to be shown,
372 * {@link #mSupportVideoCallIcon} must be {@code true}.
373 *
374 * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false}
375 * otherwise.
376 * @param listener Listener to notify when the video calling icon is clicked.
377 * @param position The position in the adapater of the video calling icon.
378 */
379 public void setShowVideoCallIcon(boolean showVideoCallIcon,
380 PhoneNumberListAdapter.Listener listener, int position) {
381 mShowVideoCallIcon = showVideoCallIcon;
382 mPhoneNumberListAdapterListener = listener;
383 mPosition = position;
384
385 if (mShowVideoCallIcon) {
386 if (mVideoCallIcon == null) {
387 mVideoCallIcon = new ImageView(getContext());
388 addView(mVideoCallIcon);
389 }
390 mVideoCallIcon.setContentDescription(getContext().getString(
391 R.string.description_search_video_call));
392 mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call);
393 mVideoCallIcon.setScaleType(ScaleType.CENTER);
394 mVideoCallIcon.setVisibility(View.VISIBLE);
395 mVideoCallIcon.setOnClickListener(new OnClickListener() {
396 @Override
397 public void onClick(View v) {
398 // Inform the adapter that the video calling icon was clicked.
399 if (mPhoneNumberListAdapterListener != null) {
400 mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition);
401 }
402 }
403 });
404 } else {
405 if (mVideoCallIcon != null) {
406 mVideoCallIcon.setVisibility(View.GONE);
407 }
408 }
409 }
410
411 /**
412 * Sets whether the view supports a video calling icon. This is independent of whether the view
413 * is actually showing an icon. Support for the video calling icon ensures that the layout
414 * leaves space for the video icon, should it be shown.
415 *
416 * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false}
417 * otherwise.
418 */
419 public void setSupportVideoCallIcon(boolean supportVideoCallIcon) {
420 mSupportVideoCallIcon = supportVideoCallIcon;
421 }
422
Chiao Cheng89437e82012-11-01 13:41:51 -0700423 @Override
424 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
425 // We will match parent's width and wrap content vertically, but make sure
426 // height is no less than listPreferredItemHeight.
427 final int specWidth = resolveSize(0, widthMeasureSpec);
Andrew Leecd366f32014-04-22 15:44:37 -0700428 final int preferredHeight = mPreferredHeight;
Chiao Cheng89437e82012-11-01 13:41:51 -0700429
430 mNameTextViewHeight = 0;
431 mPhoneticNameTextViewHeight = 0;
432 mLabelViewHeight = 0;
433 mDataViewHeight = 0;
434 mLabelAndDataViewMaxHeight = 0;
435 mSnippetTextViewHeight = 0;
436 mStatusTextViewHeight = 0;
Brian Attwell207a8772015-02-27 16:35:00 -0800437 mCheckBoxWidth = 0;
438 mCheckBoxHeight = 0;
Walter Jang379ff852016-05-22 12:17:39 -0700439 mDeleteImageButtonWidth = 0;
440 mDeleteImageButtonHeight = 0;
Chiao Cheng89437e82012-11-01 13:41:51 -0700441
442 ensurePhotoViewSize();
443
444 // Width each TextView is able to use.
Andrew Leedd28d832014-04-28 18:38:31 -0700445 int effectiveWidth;
Chiao Cheng89437e82012-11-01 13:41:51 -0700446 // All the other Views will honor the photo, so available width for them may be shrunk.
447 if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
448 effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
449 - (mPhotoViewWidth + mGapBetweenImageAndText);
450 } else {
451 effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
452 }
453
Andrew Leedd28d832014-04-28 18:38:31 -0700454 if (mIsSectionHeaderEnabled) {
Wenyi Wangb8b2fa32016-05-23 15:52:23 -0700455 effectiveWidth -= mHeaderWidth + mGapBetweenIndexerAndImage;
Andrew Leedd28d832014-04-28 18:38:31 -0700456 }
457
Tyler Gunn001d9742015-12-18 13:57:02 -0800458 if (mSupportVideoCallIcon) {
459 effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin);
460 }
461
Chiao Cheng89437e82012-11-01 13:41:51 -0700462 // Go over all visible text views and measure actual width of each of them.
463 // Also calculate their heights to get the total height for this entire view.
464
Brian Attwell207a8772015-02-27 16:35:00 -0800465 if (isVisible(mCheckBox)) {
466 mCheckBox.measure(
467 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
468 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
469 mCheckBoxWidth = mCheckBox.getMeasuredWidth();
470 mCheckBoxHeight = mCheckBox.getMeasuredHeight();
471 effectiveWidth -= mCheckBoxWidth + mGapBetweenImageAndText;
472 }
473
Walter Jang379ff852016-05-22 12:17:39 -0700474 if (isVisible(mDeleteImageButton)) {
475 mDeleteImageButton.measure(
476 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
477 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
478 mDeleteImageButtonWidth = mDeleteImageButton.getMeasuredWidth();
479 mDeleteImageButtonHeight = mDeleteImageButton.getMeasuredHeight();
480 effectiveWidth -= mDeleteImageButtonWidth + mGapBetweenImageAndText;
481 }
482
Chiao Cheng89437e82012-11-01 13:41:51 -0700483 if (isVisible(mNameTextView)) {
Paul Soulos2b1334422014-05-27 11:28:00 -0700484 // Calculate width for name text - this parallels similar measurement in onLayout.
Chiao Cheng89437e82012-11-01 13:41:51 -0700485 int nameTextWidth = effectiveWidth;
486 if (mPhotoPosition != PhotoPosition.LEFT) {
487 nameTextWidth -= mTextIndent;
488 }
489 mNameTextView.measure(
490 MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
491 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
492 mNameTextViewHeight = mNameTextView.getMeasuredHeight();
493 }
494
495 if (isVisible(mPhoneticNameTextView)) {
496 mPhoneticNameTextView.measure(
497 MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
498 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
499 mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
500 }
501
502 // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
503 // we should ellipsize both using appropriate ratio.
504 final int dataWidth;
505 final int labelWidth;
506 if (isVisible(mDataView)) {
507 if (isVisible(mLabelView)) {
508 final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
509 dataWidth = ((totalWidth * mDataViewWidthWeight)
510 / (mDataViewWidthWeight + mLabelViewWidthWeight));
511 labelWidth = ((totalWidth * mLabelViewWidthWeight) /
512 (mDataViewWidthWeight + mLabelViewWidthWeight));
513 } else {
514 dataWidth = effectiveWidth;
515 labelWidth = 0;
516 }
517 } else {
518 dataWidth = 0;
519 if (isVisible(mLabelView)) {
520 labelWidth = effectiveWidth;
521 } else {
522 labelWidth = 0;
523 }
524 }
525
526 if (isVisible(mDataView)) {
527 mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
528 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
529 mDataViewHeight = mDataView.getMeasuredHeight();
530 }
531
532 if (isVisible(mLabelView)) {
Tyler Gunn001d9742015-12-18 13:57:02 -0800533 mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST),
Chiao Cheng89437e82012-11-01 13:41:51 -0700534 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
535 mLabelViewHeight = mLabelView.getMeasuredHeight();
536 }
537 mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
538
539 if (isVisible(mSnippetView)) {
540 mSnippetView.measure(
541 MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
542 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
543 mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
544 }
545
546 // Status view height is the biggest of the text view and the presence icon
547 if (isVisible(mPresenceIcon)) {
548 mPresenceIcon.measure(
549 MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
550 MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
551 mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
552 }
553
Tyler Gunn001d9742015-12-18 13:57:02 -0800554 if (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) {
555 mVideoCallIcon.measure(
556 MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY),
557 MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY));
558 }
559
Victor Chang3e21eef2015-12-24 13:17:42 +0800560 if (isVisible(mWorkProfileIcon)) {
561 mWorkProfileIcon.measure(
562 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
563 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
564 mNameTextViewHeight =
565 Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight());
566 }
567
Chiao Cheng89437e82012-11-01 13:41:51 -0700568 if (isVisible(mStatusView)) {
569 // Presence and status are in a same row, so status will be affected by icon size.
570 final int statusWidth;
571 if (isVisible(mPresenceIcon)) {
572 statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
573 - mPresenceIconMargin);
574 } else {
575 statusWidth = effectiveWidth;
576 }
577 mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
578 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
579 mStatusTextViewHeight =
580 Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
581 }
582
583 // Calculate height including padding.
584 int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
585 mLabelAndDataViewMaxHeight +
586 mSnippetTextViewHeight + mStatusTextViewHeight);
587
588 // Make sure the height is at least as high as the photo
589 height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
590
Chiao Cheng89437e82012-11-01 13:41:51 -0700591 // Make sure height is at least the preferred height
592 height = Math.max(height, preferredHeight);
593
Andrew Lee020ba622014-04-25 15:26:35 -0700594 // Measure the header if it is visible.
595 if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) {
Chiao Cheng89437e82012-11-01 13:41:51 -0700596 mHeaderTextView.measure(
Andrew Lee020ba622014-04-25 15:26:35 -0700597 MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
Andrew Lee815ce482014-05-30 12:28:17 -0700598 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
Chiao Cheng89437e82012-11-01 13:41:51 -0700599 }
600
601 setMeasuredDimension(specWidth, height);
602 }
603
604 @Override
605 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
606 final int height = bottom - top;
607 final int width = right - left;
608
609 // Determine the vertical bounds by laying out the header first.
610 int topBound = 0;
611 int bottomBound = height;
612 int leftBound = getPaddingLeft();
613 int rightBound = width - getPaddingRight();
614
Yorke Lee50a89a52013-11-04 14:44:30 -0800615 final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);
Yorke Lee72506b72013-02-05 16:25:54 -0800616
Andrew Lee020ba622014-04-25 15:26:35 -0700617 // Put the section header on the left side of the contact view.
618 if (mIsSectionHeaderEnabled) {
619 if (mHeaderTextView != null) {
Andrew Lee815ce482014-05-30 12:28:17 -0700620 int headerHeight = mHeaderTextView.getMeasuredHeight();
621 int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop;
622
Andrew Lee020ba622014-04-25 15:26:35 -0700623 mHeaderTextView.layout(
624 isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
Andrew Lee815ce482014-05-30 12:28:17 -0700625 headerTopBound,
Andrew Lee020ba622014-04-25 15:26:35 -0700626 isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
Andrew Lee815ce482014-05-30 12:28:17 -0700627 headerTopBound + headerHeight);
Andrew Lee020ba622014-04-25 15:26:35 -0700628 }
629 if (isLayoutRtl) {
630 rightBound -= mHeaderWidth;
631 } else {
632 leftBound += mHeaderWidth;
633 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700634 }
635
Sai Cheemalapatid814b032014-06-30 14:46:25 -0700636 mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound);
637 mLeftOffset = left + leftBound;
638 mRightOffset = left + rightBound;
Andrew Leedd28d832014-04-28 18:38:31 -0700639 if (mIsSectionHeaderEnabled) {
640 if (isLayoutRtl) {
Wenyi Wangb8b2fa32016-05-23 15:52:23 -0700641 rightBound -= mGapBetweenIndexerAndImage;
Andrew Leedd28d832014-04-28 18:38:31 -0700642 } else {
Wenyi Wangb8b2fa32016-05-23 15:52:23 -0700643 leftBound += mGapBetweenIndexerAndImage;
Andrew Leedd28d832014-04-28 18:38:31 -0700644 }
645 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700646
647 if (mActivatedStateSupported && isActivated()) {
648 mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
649 }
650
Brian Attwell207a8772015-02-27 16:35:00 -0800651 if (isVisible(mCheckBox)) {
652 final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2;
653 if (mPhotoPosition == PhotoPosition.LEFT) {
654 mCheckBox.layout(rightBound - mCheckBoxWidth,
655 photoTop,
656 rightBound,
657 photoTop + mCheckBoxHeight);
658 } else {
659 mCheckBox.layout(leftBound,
660 photoTop,
661 leftBound + mCheckBoxWidth,
662 photoTop + mCheckBoxHeight);
663 }
664 }
665
Walter Jang379ff852016-05-22 12:17:39 -0700666 if (isVisible(mDeleteImageButton)) {
667 final int photoTop = topBound + (bottomBound - topBound - mDeleteImageButtonHeight) / 2;
Tingting Wang408b0232016-06-28 23:00:30 -0700668 final int mDeleteImageButtonSize = mDeleteImageButtonHeight > mDeleteImageButtonWidth
669 ? mDeleteImageButtonHeight : mDeleteImageButtonWidth;
Walter Jang379ff852016-05-22 12:17:39 -0700670 if (mPhotoPosition == PhotoPosition.LEFT) {
Tingting Wang408b0232016-06-28 23:00:30 -0700671 mDeleteImageButton.layout(rightBound - mDeleteImageButtonSize,
Walter Jang379ff852016-05-22 12:17:39 -0700672 photoTop,
673 rightBound,
Tingting Wang408b0232016-06-28 23:00:30 -0700674 photoTop + mDeleteImageButtonSize);
Walter Jang379ff852016-05-22 12:17:39 -0700675 } else {
676 mDeleteImageButton.layout(leftBound,
677 photoTop,
Tingting Wang408b0232016-06-28 23:00:30 -0700678 leftBound + mDeleteImageButtonSize,
679 photoTop + mDeleteImageButtonSize);
Walter Jang379ff852016-05-22 12:17:39 -0700680 }
681 }
682
Chiao Cheng89437e82012-11-01 13:41:51 -0700683 final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
684 if (mPhotoPosition == PhotoPosition.LEFT) {
685 // Photo is the left most view. All the other Views should on the right of the photo.
686 if (photoView != null) {
687 // Center the photo vertically
688 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
689 photoView.layout(
690 leftBound,
691 photoTop,
692 leftBound + mPhotoViewWidth,
693 photoTop + mPhotoViewHeight);
694 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
695 } else if (mKeepHorizontalPaddingForPhotoView) {
696 // Draw nothing but keep the padding.
697 leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
698 }
699 } else {
700 // Photo is the right most view. Right bound should be adjusted that way.
701 if (photoView != null) {
702 // Center the photo vertically
703 final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
704 photoView.layout(
705 rightBound - mPhotoViewWidth,
706 photoTop,
707 rightBound,
708 photoTop + mPhotoViewHeight);
709 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700710 } else if (mKeepHorizontalPaddingForPhotoView) {
711 // Draw nothing but keep the padding.
712 rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
Chiao Cheng89437e82012-11-01 13:41:51 -0700713 }
714
715 // Add indent between left-most padding and texts.
716 leftBound += mTextIndent;
717 }
718
Tyler Gunn001d9742015-12-18 13:57:02 -0800719 if (mSupportVideoCallIcon) {
720 // Place the video call button at the end of the list (e.g. take into account RTL mode).
721 if (isVisible(mVideoCallIcon)) {
722 // Center the video icon vertically
723 final int videoIconTop = topBound +
724 (bottomBound - topBound - mVideoCallIconSize) / 2;
725
726 if (!isLayoutRtl) {
727 // When photo is on left, video icon is placed on the right edge.
728 mVideoCallIcon.layout(rightBound - mVideoCallIconSize,
729 videoIconTop,
730 rightBound,
731 videoIconTop + mVideoCallIconSize);
732 } else {
733 // When photo is on right, video icon is placed on the left edge.
734 mVideoCallIcon.layout(leftBound,
735 videoIconTop,
736 leftBound + mVideoCallIconSize,
737 videoIconTop + mVideoCallIconSize);
738 }
739 }
740
741 if (mPhotoPosition == PhotoPosition.LEFT) {
742 rightBound -= (mVideoCallIconSize + mVideoCallIconMargin);
743 } else {
744 leftBound += mVideoCallIconSize + mVideoCallIconMargin;
745 }
746 }
747
748
Andrew Leeba448962014-05-29 16:20:19 -0700749 // Center text vertically, then apply the top offset.
Chiao Cheng89437e82012-11-01 13:41:51 -0700750 final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
751 mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
Andrew Leeba448962014-05-29 16:20:19 -0700752 int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop;
Chiao Cheng89437e82012-11-01 13:41:51 -0700753
Victor Chang3e21eef2015-12-24 13:17:42 +0800754 // Work Profile icon align top
755 int workProfileIconWidth = 0;
756 if (isVisible(mWorkProfileIcon)) {
757 workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
758 final int distanceFromEnd = mCheckBoxWidth > 0
759 ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
760 if (mPhotoPosition == PhotoPosition.LEFT) {
761 // When photo is on left, label is placed on the right edge of the list item.
762 mWorkProfileIcon.layout(rightBound - workProfileIconWidth - distanceFromEnd,
763 textTopBound,
764 rightBound - distanceFromEnd,
765 textTopBound + mNameTextViewHeight);
766 } else {
767 // When photo is on right, label is placed on the left of data view.
768 mWorkProfileIcon.layout(leftBound + distanceFromEnd,
769 textTopBound,
770 leftBound + workProfileIconWidth + distanceFromEnd,
771 textTopBound + mNameTextViewHeight);
772 }
773 }
774
Chiao Cheng89437e82012-11-01 13:41:51 -0700775 // Layout all text view and presence icon
776 // Put name TextView first
777 if (isVisible(mNameTextView)) {
Victor Chang3e21eef2015-12-24 13:17:42 +0800778 final int distanceFromEnd = workProfileIconWidth
779 + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
Brian Attwell207a8772015-02-27 16:35:00 -0800780 if (mPhotoPosition == PhotoPosition.LEFT) {
781 mNameTextView.layout(leftBound,
782 textTopBound,
783 rightBound - distanceFromEnd,
784 textTopBound + mNameTextViewHeight);
785 } else {
786 mNameTextView.layout(leftBound + distanceFromEnd,
787 textTopBound,
788 rightBound,
789 textTopBound + mNameTextViewHeight);
790 }
Victor Chang3e21eef2015-12-24 13:17:42 +0800791 }
792
793 if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
Chiao Cheng89437e82012-11-01 13:41:51 -0700794 textTopBound += mNameTextViewHeight;
795 }
796
797 // Presence and status
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700798 if (isLayoutRtl) {
799 int statusRightBound = rightBound;
800 if (isVisible(mPresenceIcon)) {
801 int iconWidth = mPresenceIcon.getMeasuredWidth();
802 mPresenceIcon.layout(
803 rightBound - iconWidth,
804 textTopBound,
805 rightBound,
806 textTopBound + mStatusTextViewHeight);
807 statusRightBound -= (iconWidth + mPresenceIconMargin);
808 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700809
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700810 if (isVisible(mStatusView)) {
811 mStatusView.layout(leftBound,
812 textTopBound,
813 statusRightBound,
814 textTopBound + mStatusTextViewHeight);
815 }
816 } else {
817 int statusLeftBound = leftBound;
818 if (isVisible(mPresenceIcon)) {
819 int iconWidth = mPresenceIcon.getMeasuredWidth();
820 mPresenceIcon.layout(
821 leftBound,
822 textTopBound,
823 leftBound + iconWidth,
824 textTopBound + mStatusTextViewHeight);
825 statusLeftBound += (iconWidth + mPresenceIconMargin);
826 }
827
828 if (isVisible(mStatusView)) {
829 mStatusView.layout(statusLeftBound,
830 textTopBound,
831 rightBound,
832 textTopBound + mStatusTextViewHeight);
833 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700834 }
835
836 if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
837 textTopBound += mStatusTextViewHeight;
838 }
839
840 // Rest of text views
841 int dataLeftBound = leftBound;
842 if (isVisible(mPhoneticNameTextView)) {
843 mPhoneticNameTextView.layout(leftBound,
844 textTopBound,
845 rightBound,
846 textTopBound + mPhoneticNameTextViewHeight);
847 textTopBound += mPhoneticNameTextViewHeight;
848 }
849
850 // Label and Data align bottom.
851 if (isVisible(mLabelView)) {
Tyler Gunn001d9742015-12-18 13:57:02 -0800852 if (!isLayoutRtl) {
853 mLabelView.layout(dataLeftBound,
854 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
855 rightBound,
856 textTopBound + mLabelAndDataViewMaxHeight);
857 dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
858 } else {
859 dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
Chiao Cheng89437e82012-11-01 13:41:51 -0700860 mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
861 textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
862 rightBound,
863 textTopBound + mLabelAndDataViewMaxHeight);
Tyler Gunn001d9742015-12-18 13:57:02 -0800864 rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
Chiao Cheng89437e82012-11-01 13:41:51 -0700865 }
866 }
867
868 if (isVisible(mDataView)) {
Tyler Gunn001d9742015-12-18 13:57:02 -0800869 if (!isLayoutRtl) {
870 mDataView.layout(dataLeftBound,
871 textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
872 rightBound,
873 textTopBound + mLabelAndDataViewMaxHeight);
874 } else {
875 mDataView.layout(rightBound - mDataView.getMeasuredWidth(),
876 textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
877 rightBound,
878 textTopBound + mLabelAndDataViewMaxHeight);
879 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700880 }
881 if (isVisible(mLabelView) || isVisible(mDataView)) {
882 textTopBound += mLabelAndDataViewMaxHeight;
883 }
884
885 if (isVisible(mSnippetView)) {
886 mSnippetView.layout(leftBound,
887 textTopBound,
888 rightBound,
889 textTopBound + mSnippetTextViewHeight);
890 }
891 }
892
893 @Override
894 public void adjustListItemSelectionBounds(Rect bounds) {
Andrew Lee4683e542014-06-09 16:24:10 -0700895 if (mAdjustSelectionBoundsEnabled) {
896 bounds.top += mBoundsWithoutHeader.top;
897 bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
898 bounds.left = mBoundsWithoutHeader.left;
899 bounds.right = mBoundsWithoutHeader.right;
900 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700901 }
902
903 protected boolean isVisible(View view) {
904 return view != null && view.getVisibility() == View.VISIBLE;
905 }
906
907 /**
908 * Extracts width and height from the style
909 */
910 private void ensurePhotoViewSize() {
911 if (!mPhotoViewWidthAndHeightAreReady) {
912 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
913 if (!mQuickContactEnabled && mPhotoView == null) {
914 if (!mKeepHorizontalPaddingForPhotoView) {
915 mPhotoViewWidth = 0;
916 }
917 if (!mKeepVerticalPaddingForPhotoView) {
918 mPhotoViewHeight = 0;
919 }
920 }
921
922 mPhotoViewWidthAndHeightAreReady = true;
923 }
924 }
925
Chiao Cheng89437e82012-11-01 13:41:51 -0700926 protected int getDefaultPhotoViewSize() {
927 return mDefaultPhotoViewSize;
928 }
929
930 /**
931 * Gets a LayoutParam that corresponds to the default photo size.
932 *
933 * @return A new LayoutParam.
934 */
935 private LayoutParams getDefaultPhotoLayoutParams() {
936 LayoutParams params = generateDefaultLayoutParams();
937 params.width = getDefaultPhotoViewSize();
938 params.height = params.width;
939 return params;
940 }
941
942 @Override
943 protected void drawableStateChanged() {
944 super.drawableStateChanged();
945 if (mActivatedStateSupported) {
946 mActivatedBackgroundDrawable.setState(getDrawableState());
947 }
948 }
949
950 @Override
951 protected boolean verifyDrawable(Drawable who) {
952 return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
953 }
954
955 @Override
956 public void jumpDrawablesToCurrentState() {
957 super.jumpDrawablesToCurrentState();
958 if (mActivatedStateSupported) {
959 mActivatedBackgroundDrawable.jumpToCurrentState();
960 }
961 }
962
963 @Override
964 public void dispatchDraw(Canvas canvas) {
965 if (mActivatedStateSupported && isActivated()) {
966 mActivatedBackgroundDrawable.draw(canvas);
967 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700968
969 super.dispatchDraw(canvas);
970 }
971
972 /**
Chiao Cheng89437e82012-11-01 13:41:51 -0700973 * Sets section header or makes it invisible if the title is null.
974 */
975 public void setSectionHeader(String title) {
976 if (!TextUtils.isEmpty(title)) {
977 if (mHeaderTextView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -0700978 mHeaderTextView = new TextView(getContext());
Yorke Leef3f259c2013-10-21 15:06:18 -0700979 mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle);
Andrew Lee815ce482014-05-30 12:28:17 -0700980 mHeaderTextView.setGravity(
981 ViewUtil.isViewLayoutRtl(this) ? Gravity.RIGHT : Gravity.LEFT);
Chiao Cheng89437e82012-11-01 13:41:51 -0700982 addView(mHeaderTextView);
983 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700984 setMarqueeText(mHeaderTextView, title);
985 mHeaderTextView.setVisibility(View.VISIBLE);
Chiao Cheng89437e82012-11-01 13:41:51 -0700986 mHeaderTextView.setAllCaps(true);
Andrew Lee020ba622014-04-25 15:26:35 -0700987 } else if (mHeaderTextView != null) {
988 mHeaderTextView.setVisibility(View.GONE);
Chiao Cheng89437e82012-11-01 13:41:51 -0700989 }
990 }
991
Andrew Lee020ba622014-04-25 15:26:35 -0700992 public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
993 mIsSectionHeaderEnabled = isSectionHeaderEnabled;
994 }
995
Chiao Cheng89437e82012-11-01 13:41:51 -0700996 /**
997 * Returns the quick contact badge, creating it if necessary.
998 */
999 public QuickContactBadge getQuickContact() {
1000 if (!mQuickContactEnabled) {
1001 throw new IllegalStateException("QuickContact is disabled for this view");
1002 }
1003 if (mQuickContact == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001004 mQuickContact = new QuickContactBadge(getContext());
Wenyi Wang54ea4b12015-12-16 14:18:59 -08001005 if (CompatUtils.isLollipopCompatible()) {
1006 mQuickContact.setOverlay(null);
1007 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001008 mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
1009 if (mNameTextView != null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001010 mQuickContact.setContentDescription(getContext().getString(
Chiao Cheng89437e82012-11-01 13:41:51 -07001011 R.string.description_quick_contact_for, mNameTextView.getText()));
1012 }
1013
1014 addView(mQuickContact);
1015 mPhotoViewWidthAndHeightAreReady = false;
1016 }
1017 return mQuickContact;
1018 }
1019
1020 /**
1021 * Returns the photo view, creating it if necessary.
1022 */
1023 public ImageView getPhotoView() {
1024 if (mPhotoView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001025 mPhotoView = new ImageView(getContext());
Chiao Cheng89437e82012-11-01 13:41:51 -07001026 mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
1027 // Quick contact style used above will set a background - remove it
1028 mPhotoView.setBackground(null);
1029 addView(mPhotoView);
1030 mPhotoViewWidthAndHeightAreReady = false;
1031 }
1032 return mPhotoView;
1033 }
1034
1035 /**
1036 * Removes the photo view.
1037 */
1038 public void removePhotoView() {
1039 removePhotoView(false, true);
1040 }
1041
1042 /**
1043 * Removes the photo view.
1044 *
1045 * @param keepHorizontalPadding True means data on the right side will have
1046 * padding on left, pretending there is still a photo view.
1047 * @param keepVerticalPadding True means the View will have some height
1048 * enough for accommodating a photo view.
1049 */
1050 public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
1051 mPhotoViewWidthAndHeightAreReady = false;
1052 mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
1053 mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
1054 if (mPhotoView != null) {
1055 removeView(mPhotoView);
1056 mPhotoView = null;
1057 }
1058 if (mQuickContact != null) {
1059 removeView(mQuickContact);
1060 mQuickContact = null;
1061 }
1062 }
1063
1064 /**
1065 * Sets a word prefix that will be highlighted if encountered in fields like
Christine Chen3efbe592013-07-08 18:05:03 -07001066 * name and search snippet. This will disable the mask highlighting for names.
Chiao Cheng89437e82012-11-01 13:41:51 -07001067 * <p>
1068 * NOTE: must be all upper-case
1069 */
Chiao Chenga1554ef2012-12-21 15:45:54 -08001070 public void setHighlightedPrefix(String upperCasePrefix) {
Chiao Cheng89437e82012-11-01 13:41:51 -07001071 mHighlightedPrefix = upperCasePrefix;
Christine Chen3efbe592013-07-08 18:05:03 -07001072 }
1073
1074 /**
Christine Chenccba9502013-07-12 12:04:54 -07001075 * Clears previously set highlight sequences for the view.
Christine Chen3efbe592013-07-08 18:05:03 -07001076 */
Christine Chenccba9502013-07-12 12:04:54 -07001077 public void clearHighlightSequences() {
1078 mNameHighlightSequence.clear();
1079 mNumberHighlightSequence.clear();
Christine Chen3efbe592013-07-08 18:05:03 -07001080 mHighlightedPrefix = null;
1081 }
1082
1083 /**
Christine Chenccba9502013-07-12 12:04:54 -07001084 * Adds a highlight sequence to the name highlighter.
1085 * @param start The start position of the highlight sequence.
1086 * @param end The end position of the highlight sequence.
Christine Chen3efbe592013-07-08 18:05:03 -07001087 */
Christine Chenccba9502013-07-12 12:04:54 -07001088 public void addNameHighlightSequence(int start, int end) {
1089 mNameHighlightSequence.add(new HighlightSequence(start, end));
1090 }
1091
1092 /**
1093 * Adds a highlight sequence to the number highlighter.
1094 * @param start The start position of the highlight sequence.
1095 * @param end The end position of the highlight sequence.
1096 */
1097 public void addNumberHighlightSequence(int start, int end) {
1098 mNumberHighlightSequence.add(new HighlightSequence(start, end));
Chiao Cheng89437e82012-11-01 13:41:51 -07001099 }
1100
1101 /**
1102 * Returns the text view for the contact name, creating it if necessary.
1103 */
1104 public TextView getNameTextView() {
1105 if (mNameTextView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001106 mNameTextView = new TextView(getContext());
Chiao Cheng89437e82012-11-01 13:41:51 -07001107 mNameTextView.setSingleLine(true);
1108 mNameTextView.setEllipsize(getTextEllipsis());
Nancy Chenae961762014-05-23 11:03:21 -07001109 mNameTextView.setTextColor(mNameTextViewTextColor);
Sai Cheemalapati7c776a52014-05-27 16:37:15 -07001110 mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
1111 mNameTextViewTextSize);
Chiao Cheng89437e82012-11-01 13:41:51 -07001112 // Manually call setActivated() since this view may be added after the first
1113 // setActivated() call toward this whole item view.
1114 mNameTextView.setActivated(isActivated());
1115 mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -07001116 mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
1117 mNameTextView.setId(R.id.cliv_name_textview);
Wenyi Wang54ea4b12015-12-16 14:18:59 -08001118 if (CompatUtils.isLollipopCompatible()) {
1119 mNameTextView.setElegantTextHeight(false);
1120 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001121 addView(mNameTextView);
1122 }
1123 return mNameTextView;
1124 }
1125
1126 /**
1127 * Adds or updates a text view for the phonetic name.
1128 */
1129 public void setPhoneticName(char[] text, int size) {
1130 if (text == null || size == 0) {
1131 if (mPhoneticNameTextView != null) {
1132 mPhoneticNameTextView.setVisibility(View.GONE);
1133 }
1134 } else {
1135 getPhoneticNameTextView();
1136 setMarqueeText(mPhoneticNameTextView, text, size);
1137 mPhoneticNameTextView.setVisibility(VISIBLE);
1138 }
1139 }
1140
1141 /**
1142 * Returns the text view for the phonetic name, creating it if necessary.
1143 */
1144 public TextView getPhoneticNameTextView() {
1145 if (mPhoneticNameTextView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001146 mPhoneticNameTextView = new TextView(getContext());
Chiao Cheng89437e82012-11-01 13:41:51 -07001147 mPhoneticNameTextView.setSingleLine(true);
1148 mPhoneticNameTextView.setEllipsize(getTextEllipsis());
Yorke Leef3f259c2013-10-21 15:06:18 -07001149 mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
Walter Jang0b2f8cf2014-11-12 13:45:19 -08001150 mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
Chiao Cheng89437e82012-11-01 13:41:51 -07001151 mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
1152 mPhoneticNameTextView.setActivated(isActivated());
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -07001153 mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview);
Chiao Cheng89437e82012-11-01 13:41:51 -07001154 addView(mPhoneticNameTextView);
1155 }
1156 return mPhoneticNameTextView;
1157 }
1158
1159 /**
1160 * Adds or updates a text view for the data label.
1161 */
1162 public void setLabel(CharSequence text) {
1163 if (TextUtils.isEmpty(text)) {
1164 if (mLabelView != null) {
1165 mLabelView.setVisibility(View.GONE);
1166 }
1167 } else {
1168 getLabelView();
1169 setMarqueeText(mLabelView, text);
1170 mLabelView.setVisibility(VISIBLE);
1171 }
1172 }
1173
1174 /**
1175 * Returns the text view for the data label, creating it if necessary.
1176 */
1177 public TextView getLabelView() {
1178 if (mLabelView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001179 mLabelView = new TextView(getContext());
Tyler Gunn001d9742015-12-18 13:57:02 -08001180 mLabelView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
1181 LayoutParams.WRAP_CONTENT));
1182
Chiao Cheng89437e82012-11-01 13:41:51 -07001183 mLabelView.setSingleLine(true);
1184 mLabelView.setEllipsize(getTextEllipsis());
Yorke Lee796824d2013-12-02 15:41:42 -08001185 mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
Chiao Cheng89437e82012-11-01 13:41:51 -07001186 if (mPhotoPosition == PhotoPosition.LEFT) {
Chiao Cheng89437e82012-11-01 13:41:51 -07001187 mLabelView.setAllCaps(true);
Chiao Cheng89437e82012-11-01 13:41:51 -07001188 } else {
1189 mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
1190 }
1191 mLabelView.setActivated(isActivated());
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -07001192 mLabelView.setId(R.id.cliv_label_textview);
Chiao Cheng89437e82012-11-01 13:41:51 -07001193 addView(mLabelView);
1194 }
1195 return mLabelView;
1196 }
1197
1198 /**
1199 * Adds or updates a text view for the data element.
1200 */
Christine Chen3efbe592013-07-08 18:05:03 -07001201 public void setData(char[] text, int size) {
Chiao Cheng89437e82012-11-01 13:41:51 -07001202 if (text == null || size == 0) {
1203 if (mDataView != null) {
1204 mDataView.setVisibility(View.GONE);
1205 }
1206 } else {
1207 getDataView();
1208 setMarqueeText(mDataView, text, size);
1209 mDataView.setVisibility(VISIBLE);
Christine Chen3efbe592013-07-08 18:05:03 -07001210 }
1211 }
1212
1213 /**
1214 * Sets phone number for a list item. This takes care of number highlighting if the highlight
1215 * mask exists.
1216 */
Andrew Lee32e17da2014-03-31 12:44:34 -07001217 public void setPhoneNumber(String text, String countryIso) {
Christine Chen3efbe592013-07-08 18:05:03 -07001218 if (text == null) {
1219 if (mDataView != null) {
1220 mDataView.setVisibility(View.GONE);
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -07001221 }
Christine Chen3efbe592013-07-08 18:05:03 -07001222 } else {
1223 getDataView();
Andrew Lee32e17da2014-03-31 12:44:34 -07001224
Yorke Lee69f471a2014-04-08 15:00:47 -07001225 // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
1226 // mDataView. Make sure that determination of the highlight sequences are done only
1227 // after number formatting.
Andrew Lee32e17da2014-03-31 12:44:34 -07001228
Christine Chen3efbe592013-07-08 18:05:03 -07001229 // Sets phone number texts for display after highlighting it, if applicable.
Andrew Lee32e17da2014-03-31 12:44:34 -07001230 // CharSequence textToSet = text;
Christine Chenccba9502013-07-12 12:04:54 -07001231 final SpannableString textToSet = new SpannableString(text);
1232
1233 if (mNumberHighlightSequence.size() != 0) {
1234 final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
1235 mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start,
1236 highlightSequence.end);
Christine Chen3efbe592013-07-08 18:05:03 -07001237 }
Christine Chenccba9502013-07-12 12:04:54 -07001238
Christine Chen3efbe592013-07-08 18:05:03 -07001239 setMarqueeText(mDataView, textToSet);
1240 mDataView.setVisibility(VISIBLE);
1241
1242 // We have a phone number as "mDataView" so make it always LTR and VIEW_START
1243 mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
1244 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
Chiao Cheng89437e82012-11-01 13:41:51 -07001245 }
1246 }
1247
1248 private void setMarqueeText(TextView textView, char[] text, int size) {
1249 if (getTextEllipsis() == TruncateAt.MARQUEE) {
1250 setMarqueeText(textView, new String(text, 0, size));
1251 } else {
1252 textView.setText(text, 0, size);
1253 }
1254 }
1255
1256 private void setMarqueeText(TextView textView, CharSequence text) {
1257 if (getTextEllipsis() == TruncateAt.MARQUEE) {
1258 // To show MARQUEE correctly (with END effect during non-active state), we need
1259 // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
1260 final SpannableString spannable = new SpannableString(text);
1261 spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
1262 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1263 textView.setText(spannable);
1264 } else {
1265 textView.setText(text);
1266 }
1267 }
1268
1269 /**
Wenyi Wange4e359f2016-04-02 14:30:02 -07001270 * Returns the {@link AppCompatCheckBox} view, creating it if necessary.
Brian Attwell207a8772015-02-27 16:35:00 -08001271 */
Wenyi Wange4e359f2016-04-02 14:30:02 -07001272 public AppCompatCheckBox getCheckBox() {
Brian Attwell207a8772015-02-27 16:35:00 -08001273 if (mCheckBox == null) {
Wenyi Wange4e359f2016-04-02 14:30:02 -07001274 mCheckBox = new AppCompatCheckBox(getContext());
Brian Attwell207a8772015-02-27 16:35:00 -08001275 // Make non-focusable, so the rest of the ContactListItemView can be clicked.
1276 mCheckBox.setFocusable(false);
1277 addView(mCheckBox);
1278 }
1279 return mCheckBox;
1280 }
1281
1282 /**
Walter Jang379ff852016-05-22 12:17:39 -07001283 * Returns the {@link AppCompatImageButton} delete button, creating it if necessary.
1284 */
Tingting Wang408b0232016-06-28 23:00:30 -07001285 public AppCompatImageButton getDeleteImageButton(
1286 final MultiSelectEntryContactListAdapter.DeleteContactListener listener,
1287 final int position) {
Walter Jang379ff852016-05-22 12:17:39 -07001288 if (mDeleteImageButton == null) {
1289 mDeleteImageButton = new AppCompatImageButton(getContext());
Walter Jang9a6330e2016-06-24 12:19:32 -07001290 mDeleteImageButton.setImageResource(R.drawable.ic_cancel_black_24dp);
Tingting Wang408b0232016-06-28 23:00:30 -07001291 mDeleteImageButton.setScaleType(ScaleType.CENTER);
Walter Jang379ff852016-05-22 12:17:39 -07001292 mDeleteImageButton.setBackgroundColor(Color.TRANSPARENT);
1293 addView(mDeleteImageButton);
1294 }
Tingting Wang408b0232016-06-28 23:00:30 -07001295 // Reset onClickListener because after reloading the view, position might be changed.
1296 mDeleteImageButton.setOnClickListener(new OnClickListener() {
1297 @Override
1298 public void onClick(View v) {
1299 // Inform the adapter that delete icon was clicked.
1300 if (listener != null) {
1301 listener.onContactDeleteClicked(position);
1302 }
1303 }
1304 });
Walter Jang379ff852016-05-22 12:17:39 -07001305 return mDeleteImageButton;
1306 }
1307
1308 /**
Chiao Cheng89437e82012-11-01 13:41:51 -07001309 * Returns the text view for the data text, creating it if necessary.
1310 */
1311 public TextView getDataView() {
1312 if (mDataView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001313 mDataView = new TextView(getContext());
Chiao Cheng89437e82012-11-01 13:41:51 -07001314 mDataView.setSingleLine(true);
1315 mDataView.setEllipsize(getTextEllipsis());
Yorke Lee796824d2013-12-02 15:41:42 -08001316 mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
Walter Jang0b2f8cf2014-11-12 13:45:19 -08001317 mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
Chiao Cheng89437e82012-11-01 13:41:51 -07001318 mDataView.setActivated(isActivated());
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -07001319 mDataView.setId(R.id.cliv_data_view);
Wenyi Wang54ea4b12015-12-16 14:18:59 -08001320 if (CompatUtils.isLollipopCompatible()) {
1321 mDataView.setElegantTextHeight(false);
1322 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001323 addView(mDataView);
1324 }
1325 return mDataView;
1326 }
1327
1328 /**
1329 * Adds or updates a text view for the search snippet.
1330 */
1331 public void setSnippet(String text) {
1332 if (TextUtils.isEmpty(text)) {
1333 if (mSnippetView != null) {
1334 mSnippetView.setVisibility(View.GONE);
1335 }
1336 } else {
Christine Chenccba9502013-07-12 12:04:54 -07001337 mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
Chiao Cheng89437e82012-11-01 13:41:51 -07001338 mSnippetView.setVisibility(VISIBLE);
Walter Jang1a21fe52014-10-15 18:32:44 -07001339 if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
1340 // Give the text-to-speech engine a hint that it's a phone number
Wenyi Wang40346982015-11-18 14:58:42 -08001341 mSnippetView.setContentDescription(
1342 PhoneNumberUtilsCompat.createTtsSpannable(text));
Walter Jang1a21fe52014-10-15 18:32:44 -07001343 } else {
1344 mSnippetView.setContentDescription(null);
1345 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001346 }
1347 }
1348
1349 /**
1350 * Returns the text view for the search snippet, creating it if necessary.
1351 */
1352 public TextView getSnippetView() {
1353 if (mSnippetView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001354 mSnippetView = new TextView(getContext());
Chiao Cheng89437e82012-11-01 13:41:51 -07001355 mSnippetView.setSingleLine(true);
1356 mSnippetView.setEllipsize(getTextEllipsis());
Yorke Leef3f259c2013-10-21 15:06:18 -07001357 mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
Walter Jang0b2f8cf2014-11-12 13:45:19 -08001358 mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
Chiao Cheng89437e82012-11-01 13:41:51 -07001359 mSnippetView.setActivated(isActivated());
1360 addView(mSnippetView);
1361 }
1362 return mSnippetView;
1363 }
1364
1365 /**
1366 * Returns the text view for the status, creating it if necessary.
1367 */
1368 public TextView getStatusView() {
1369 if (mStatusView == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001370 mStatusView = new TextView(getContext());
Chiao Cheng89437e82012-11-01 13:41:51 -07001371 mStatusView.setSingleLine(true);
1372 mStatusView.setEllipsize(getTextEllipsis());
Yorke Leef3f259c2013-10-21 15:06:18 -07001373 mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
Chiao Cheng89437e82012-11-01 13:41:51 -07001374 mStatusView.setTextColor(mSecondaryTextColor);
1375 mStatusView.setActivated(isActivated());
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -07001376 mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
Chiao Cheng89437e82012-11-01 13:41:51 -07001377 addView(mStatusView);
1378 }
1379 return mStatusView;
1380 }
1381
1382 /**
Chiao Cheng89437e82012-11-01 13:41:51 -07001383 * Adds or updates a text view for the status.
1384 */
1385 public void setStatus(CharSequence text) {
1386 if (TextUtils.isEmpty(text)) {
1387 if (mStatusView != null) {
1388 mStatusView.setVisibility(View.GONE);
1389 }
1390 } else {
1391 getStatusView();
1392 setMarqueeText(mStatusView, text);
1393 mStatusView.setVisibility(VISIBLE);
1394 }
1395 }
1396
1397 /**
1398 * Adds or updates the presence icon view.
1399 */
1400 public void setPresence(Drawable icon) {
1401 if (icon != null) {
1402 if (mPresenceIcon == null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001403 mPresenceIcon = new ImageView(getContext());
Chiao Cheng89437e82012-11-01 13:41:51 -07001404 addView(mPresenceIcon);
1405 }
1406 mPresenceIcon.setImageDrawable(icon);
1407 mPresenceIcon.setScaleType(ScaleType.CENTER);
1408 mPresenceIcon.setVisibility(View.VISIBLE);
1409 } else {
1410 if (mPresenceIcon != null) {
1411 mPresenceIcon.setVisibility(View.GONE);
1412 }
1413 }
1414 }
1415
Victor Chang3e21eef2015-12-24 13:17:42 +08001416 /**
1417 * Set to display work profile icon or not
1418 *
1419 * @param enabled set to display work profile icon or not
1420 */
1421 public void setWorkProfileIconEnabled(boolean enabled) {
1422 if (mWorkProfileIcon != null) {
1423 mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
1424 } else if (enabled) {
1425 mWorkProfileIcon = new ImageView(getContext());
1426 addView(mWorkProfileIcon);
1427 mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
1428 mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
1429 mWorkProfileIcon.setVisibility(View.VISIBLE);
1430 }
1431 }
1432
Chiao Cheng89437e82012-11-01 13:41:51 -07001433 private TruncateAt getTextEllipsis() {
1434 return TruncateAt.MARQUEE;
1435 }
1436
1437 public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
1438 CharSequence name = cursor.getString(nameColumnIndex);
Yorke Lee64913842013-08-28 12:14:51 -07001439 setDisplayName(name);
1440
1441 // Since the quick contact content description is derived from the display name and there is
1442 // no guarantee that when the quick contact is initialized the display name is already set,
1443 // do it here too.
1444 if (mQuickContact != null) {
Yorke Leef3f259c2013-10-21 15:06:18 -07001445 mQuickContact.setContentDescription(getContext().getString(
Yorke Lee64913842013-08-28 12:14:51 -07001446 R.string.description_quick_contact_for, mNameTextView.getText()));
1447 }
1448 }
1449
1450 public void setDisplayName(CharSequence name, boolean highlight) {
1451 if (!TextUtils.isEmpty(name) && highlight) {
1452 clearHighlightSequences();
1453 addNameHighlightSequence(0, name.length());
1454 }
1455 setDisplayName(name);
1456 }
1457
1458 public void setDisplayName(CharSequence name) {
Chiao Cheng89437e82012-11-01 13:41:51 -07001459 if (!TextUtils.isEmpty(name)) {
Christine Chen3efbe592013-07-08 18:05:03 -07001460 // Chooses the available highlighting method for highlighting.
1461 if (mHighlightedPrefix != null) {
1462 name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
Christine Chenccba9502013-07-12 12:04:54 -07001463 } else if (mNameHighlightSequence.size() != 0) {
1464 final SpannableString spannableName = new SpannableString(name);
1465 for (HighlightSequence highlightSequence : mNameHighlightSequence) {
1466 mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start,
1467 highlightSequence.end);
1468 }
1469 name = spannableName;
Christine Chen3efbe592013-07-08 18:05:03 -07001470 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001471 } else {
1472 name = mUnknownNameText;
1473 }
1474 setMarqueeText(getNameTextView(), name);
Walter Jang1a21fe52014-10-15 18:32:44 -07001475
1476 if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
1477 // Give the text-to-speech engine a hint that it's a phone number
Tingting Wang6bb619d2015-11-05 17:47:33 -08001478 mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
Walter Jang1a21fe52014-10-15 18:32:44 -07001479 mNameTextView.setContentDescription(
Wenyi Wang40346982015-11-18 14:58:42 -08001480 PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
Walter Jang1a21fe52014-10-15 18:32:44 -07001481 } else {
Qi Wang8a2f9f62016-03-03 14:05:51 -08001482 // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
1483 // of the name into two separate parts.
1484 mNameTextView.setContentDescription(name.toString());
Walter Jang1a21fe52014-10-15 18:32:44 -07001485 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001486 }
1487
Brian Attwell207a8772015-02-27 16:35:00 -08001488 public void hideCheckBox() {
1489 if (mCheckBox != null) {
1490 removeView(mCheckBox);
1491 mCheckBox = null;
1492 }
1493 }
1494
Walter Jang379ff852016-05-22 12:17:39 -07001495 public void hideDeleteImageButton() {
1496 if (mDeleteImageButton != null) {
1497 removeView(mDeleteImageButton);
1498 mDeleteImageButton = null;
1499 }
1500 }
1501
Chiao Cheng89437e82012-11-01 13:41:51 -07001502 public void hideDisplayName() {
1503 if (mNameTextView != null) {
1504 removeView(mNameTextView);
1505 mNameTextView = null;
1506 }
1507 }
1508
1509 public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
1510 cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
1511 int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
1512 if (phoneticNameSize != 0) {
1513 setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
1514 } else {
1515 setPhoneticName(null, 0);
1516 }
1517 }
1518
1519 public void hidePhoneticName() {
1520 if (mPhoneticNameTextView != null) {
1521 removeView(mPhoneticNameTextView);
1522 mPhoneticNameTextView = null;
1523 }
1524 }
1525
1526 /**
1527 * Sets the proper icon (star or presence or nothing) and/or status message.
1528 */
1529 public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
1530 int contactStatusColumnIndex) {
1531 Drawable icon = null;
1532 int presence = 0;
1533 if (!cursor.isNull(presenceColumnIndex)) {
1534 presence = cursor.getInt(presenceColumnIndex);
1535 icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
1536 }
1537 setPresence(icon);
1538
1539 String statusMessage = null;
1540 if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
1541 statusMessage = cursor.getString(contactStatusColumnIndex);
1542 }
1543 // If there is no status message from the contact, but there was a presence value, then use
1544 // the default status message string
1545 if (statusMessage == null && presence != 0) {
1546 statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
1547 }
1548 setStatus(statusMessage);
1549 }
1550
1551 /**
Walter Jangbad5e4b2016-02-13 15:50:21 -08001552 * Shows search snippet for email and phone number matches.
1553 */
1554 public void showSnippet(Cursor cursor, String query, int snippetColumn) {
1555 // TODO: this does not properly handle phone numbers with control characters
1556 // For example if the phone number is 444-5555, the search query 4445 will match the
1557 // number since we normalize it before querying CP2 but the snippet will fail since
1558 // the portion to be highlighted is 444-5 not 4445.
1559 final String snippet = cursor.getString(snippetColumn);
1560 if (snippet == null) {
1561 setSnippet(null);
1562 return;
1563 }
1564 final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0
1565 ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null;
1566 if (snippet.equals(displayName)) {
1567 // If the snippet exactly matches the display name (i.e. the phone number or email
1568 // address is being used as the display name) then no snippet is necessary
1569 setSnippet(null);
1570 return;
1571 }
1572 // Show the snippet with the part of the query that matched it
1573 setSnippet(updateSnippet(snippet, query, displayName));
1574 }
1575
1576 /**
Chiao Cheng89437e82012-11-01 13:41:51 -07001577 * Shows search snippet.
1578 */
1579 public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
Walter Jang54564402016-01-18 11:56:19 -08001580 if (cursor.getColumnCount() <= summarySnippetColumnIndex
1581 || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
Chiao Cheng89437e82012-11-01 13:41:51 -07001582 setSnippet(null);
1583 return;
1584 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001585
Chiao Cheng901c5e52012-12-19 16:06:37 -08001586 String snippet = cursor.getString(summarySnippetColumnIndex);
Chiao Chengecba27e2012-12-27 17:14:53 -08001587
Chiao Cheng89437e82012-11-01 13:41:51 -07001588 // Do client side snippeting if provider didn't do it
Chiao Cheng901c5e52012-12-19 16:06:37 -08001589 final Bundle extras = cursor.getExtras();
Chiao Cheng89437e82012-11-01 13:41:51 -07001590 if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
Chiao Cheng89437e82012-11-01 13:41:51 -07001591
Chiao Cheng901c5e52012-12-19 16:06:37 -08001592 final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
Chiao Cheng89437e82012-11-01 13:41:51 -07001593
Chiao Chengecba27e2012-12-27 17:14:53 -08001594 String displayName = null;
1595 int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
1596 if (displayNameIndex >= 0) {
1597 displayName = cursor.getString(displayNameIndex);
Chiao Cheng901c5e52012-12-19 16:06:37 -08001598 }
Chiao Chengecba27e2012-12-27 17:14:53 -08001599
1600 snippet = updateSnippet(snippet, query, displayName);
1601
Chiao Cheng901c5e52012-12-19 16:06:37 -08001602 } else {
Chiao Cheng901c5e52012-12-19 16:06:37 -08001603 if (snippet != null) {
1604 int from = 0;
1605 int to = snippet.length();
1606 int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
1607 if (start == -1) {
1608 snippet = null;
1609 } else {
1610 int firstNl = snippet.lastIndexOf('\n', start);
1611 if (firstNl != -1) {
1612 from = firstNl + 1;
1613 }
1614 int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
1615 if (end != -1) {
1616 int lastNl = snippet.indexOf('\n', end);
1617 if (lastNl != -1) {
1618 to = lastNl;
1619 }
1620 }
1621
1622 StringBuilder sb = new StringBuilder();
1623 for (int i = from; i < to; i++) {
1624 char c = snippet.charAt(i);
1625 if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
1626 c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
1627 sb.append(c);
1628 }
1629 }
1630 snippet = sb.toString();
1631 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001632 }
1633 }
Chiao Chengecba27e2012-12-27 17:14:53 -08001634
Chiao Cheng89437e82012-11-01 13:41:51 -07001635 setSnippet(snippet);
1636 }
1637
Chiao Chengecba27e2012-12-27 17:14:53 -08001638 /**
1639 * Used for deferred snippets from the database. The contents come back as large strings which
1640 * need to be extracted for display.
1641 *
1642 * @param snippet The snippet from the database.
1643 * @param query The search query substring.
1644 * @param displayName The contact display name.
1645 * @return The proper snippet to display.
1646 */
1647 private String updateSnippet(String snippet, String query, String displayName) {
1648
1649 if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
1650 return null;
1651 }
1652 query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
1653
1654 // If the display name already contains the query term, return empty - snippets should
1655 // not be needed in that case.
1656 if (!TextUtils.isEmpty(displayName)) {
1657 final String lowerDisplayName = displayName.toLowerCase();
1658 final List<String> nameTokens = split(lowerDisplayName);
1659 for (String nameToken : nameTokens) {
1660 if (nameToken.startsWith(query)) {
1661 return null;
1662 }
1663 }
1664 }
1665
1666 // The snippet may contain multiple data lines.
1667 // Show the first line that matches the query.
1668 final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
1669
1670 if (matched != null && matched.line != null) {
1671 // Tokenize for long strings since the match may be at the end of it.
1672 // Skip this part for short strings since the whole string will be displayed.
1673 // Most contact strings are short so the snippetize method will be called infrequently.
1674 final int lengthThreshold = getResources().getInteger(
1675 R.integer.snippet_length_before_tokenize);
1676 if (matched.line.length() > lengthThreshold) {
1677 return snippetize(matched.line, matched.startIndex, lengthThreshold);
1678 } else {
1679 return matched.line;
1680 }
1681 }
1682
1683 // No match found.
1684 return null;
1685 }
1686
1687 private String snippetize(String line, int matchIndex, int maxLength) {
1688 // Show up to maxLength characters. But we only show full tokens so show the last full token
1689 // up to maxLength characters. So as many starting tokens as possible before trying ending
1690 // tokens.
1691 int remainingLength = maxLength;
1692 int tempRemainingLength = remainingLength;
1693
1694 // Start the end token after the matched query.
1695 int index = matchIndex;
1696 int endTokenIndex = index;
1697
1698 // Find the match token first.
1699 while (index < line.length()) {
1700 if (!Character.isLetterOrDigit(line.charAt(index))) {
1701 endTokenIndex = index;
1702 remainingLength = tempRemainingLength;
1703 break;
1704 }
1705 tempRemainingLength--;
1706 index++;
1707 }
1708
1709 // Find as much content before the match.
1710 index = matchIndex - 1;
1711 tempRemainingLength = remainingLength;
1712 int startTokenIndex = matchIndex;
1713 while (index > -1 && tempRemainingLength > 0) {
1714 if (!Character.isLetterOrDigit(line.charAt(index))) {
1715 startTokenIndex = index;
1716 remainingLength = tempRemainingLength;
1717 }
1718 tempRemainingLength--;
1719 index--;
1720 }
1721
1722 index = endTokenIndex;
1723 tempRemainingLength = remainingLength;
1724 // Find remaining content at after match.
1725 while (index < line.length() && tempRemainingLength > 0) {
1726 if (!Character.isLetterOrDigit(line.charAt(index))) {
1727 endTokenIndex = index;
1728 }
1729 tempRemainingLength--;
1730 index++;
1731 }
1732 // Append ellipse if there is content before or after.
1733 final StringBuilder sb = new StringBuilder();
1734 if (startTokenIndex > 0) {
1735 sb.append("...");
1736 }
1737 sb.append(line.substring(startTokenIndex, endTokenIndex));
1738 if (endTokenIndex < line.length()) {
1739 sb.append("...");
1740 }
1741 return sb.toString();
1742 }
1743
Chiao Cheng901c5e52012-12-19 16:06:37 -08001744 private static final Pattern SPLIT_PATTERN = Pattern.compile(
1745 "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
1746
1747 /**
1748 * Helper method for splitting a string into tokens. The lists passed in are populated with
1749 * the
1750 * tokens and offsets into the content of each token. The tokenization function parses e-mail
1751 * addresses as a single token; otherwise it splits on any non-alphanumeric character.
1752 *
1753 * @param content Content to split.
1754 * @return List of token strings.
1755 */
1756 private static List<String> split(String content) {
1757 final Matcher matcher = SPLIT_PATTERN.matcher(content);
1758 final ArrayList<String> tokens = Lists.newArrayList();
1759 while (matcher.find()) {
1760 tokens.add(matcher.group());
1761 }
1762 return tokens;
1763 }
1764
Chiao Cheng89437e82012-11-01 13:41:51 -07001765 /**
Jay Shrauner138515a2013-07-26 09:32:27 -07001766 * Shows data element.
Chiao Cheng89437e82012-11-01 13:41:51 -07001767 */
1768 public void showData(Cursor cursor, int dataColumnIndex) {
1769 cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
Jay Shrauner138515a2013-07-26 09:32:27 -07001770 setData(mDataBuffer.data, mDataBuffer.sizeCopied);
1771 }
1772
Chiao Cheng89437e82012-11-01 13:41:51 -07001773 public void setActivatedStateSupported(boolean flag) {
1774 this.mActivatedStateSupported = flag;
1775 }
1776
Andrew Lee4683e542014-06-09 16:24:10 -07001777 public void setAdjustSelectionBoundsEnabled(boolean enabled) {
1778 mAdjustSelectionBoundsEnabled = enabled;
1779 }
1780
Chiao Cheng89437e82012-11-01 13:41:51 -07001781 @Override
1782 public void requestLayout() {
1783 // We will assume that once measured this will not need to resize
1784 // itself, so there is no need to pass the layout request to the parent
1785 // view (ListView).
1786 forceLayout();
1787 }
1788
1789 public void setPhotoPosition(PhotoPosition photoPosition) {
1790 mPhotoPosition = photoPosition;
1791 }
1792
1793 public PhotoPosition getPhotoPosition() {
1794 return mPhotoPosition;
1795 }
1796
1797 /**
Andrew Leedddf3562015-03-20 17:57:47 -07001798 * Set drawable resources directly for the drawable resource of the photo view.
Yorke Lee64913842013-08-28 12:14:51 -07001799 *
Andrew Leedddf3562015-03-20 17:57:47 -07001800 * @param drawableId Id of drawable resource.
Yorke Lee64913842013-08-28 12:14:51 -07001801 */
Andrew Leedddf3562015-03-20 17:57:47 -07001802 public void setDrawableResource(int drawableId) {
1803 ImageView photo = getPhotoView();
Yorke Lee64913842013-08-28 12:14:51 -07001804 photo.setScaleType(ImageView.ScaleType.CENTER);
Wenyi Wang54ea4b12015-12-16 14:18:59 -08001805 final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
1806 final int iconColor =
1807 ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
1808 if (CompatUtils.isLollipopCompatible()) {
1809 photo.setImageDrawable(drawable);
1810 photo.setImageTintList(ColorStateList.valueOf(iconColor));
1811 } else {
1812 final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
1813 DrawableCompat.setTint(drawableWrapper, iconColor);
1814 photo.setImageDrawable(drawableWrapper);
1815 }
Yorke Lee64913842013-08-28 12:14:51 -07001816 }
Yorke Lee1c171fa2013-09-06 13:53:49 -07001817
1818 @Override
1819 public boolean onTouchEvent(MotionEvent event) {
1820 final float x = event.getX();
1821 final float y = event.getY();
Yorke Lee68ec6a12013-11-13 14:12:08 -08001822 // If the touch event's coordinates are not within the view's header, then delegate
1823 // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
1824 // and ignore the touch event.
Yorke Lee62ff5cd2013-12-03 13:55:34 -08001825 if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
Yorke Lee1c171fa2013-09-06 13:53:49 -07001826 return super.onTouchEvent(event);
1827 } else {
1828 return true;
1829 }
1830 }
Yorke Lee62ff5cd2013-12-03 13:55:34 -08001831
1832 private final boolean pointIsInView(float localX, float localY) {
Sai Cheemalapatid814b032014-06-30 14:46:25 -07001833 return localX >= mLeftOffset && localX < mRightOffset
Yorke Lee3174f562014-03-07 12:58:58 -08001834 && localY >= 0 && localY < (getBottom() - getTop());
Yorke Lee62ff5cd2013-12-03 13:55:34 -08001835 }
Chiao Cheng89437e82012-11-01 13:41:51 -07001836}