/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.common.list;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.SearchSnippets;
import android.support.annotation.IntDef;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView.SelectionBoundsAdjuster;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.QuickContactBadge;
import android.widget.TextView;
import com.android.contacts.common.ContactPresenceIconUtil;
import com.android.contacts.common.ContactStatusUtil;
import com.android.contacts.common.R;
import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
import com.android.contacts.common.format.TextHighlighter;
import com.android.contacts.common.list.PhoneNumberListAdapter.Listener;
import com.android.contacts.common.util.ContactDisplayUtils;
import com.android.contacts.common.util.SearchUtil;
import com.android.dialer.compat.CompatUtils;
import com.android.dialer.util.ViewUtil;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A custom view for an item in the contact list. The view contains the contact's photo, a set of
* text views (for name, status, etc...) and icons for presence and call. The view uses no XML file
* for layout and all the measurements and layouts are done in the onMeasure and onLayout methods.
*
*
The layout puts the contact's photo on the right side of the view, the call icon (if present)
* to the left of the photo, the text lines are aligned to the left and the presence icon (if
* present) is set to the left of the status line.
*
*
The layout also supports a header (used as a header of a group of contacts) that is above the
* contact's data and a divider between contact view.
*/
public class ContactListItemView extends ViewGroup implements SelectionBoundsAdjuster {
/** IntDef for indices of ViewPager tabs. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({NONE, VIDEO, CALL_AND_SHARE})
public @interface CallToAction {}
public static final int NONE = 0;
public static final int VIDEO = 1;
public static final int CALL_AND_SHARE = 2;
private static final Pattern SPLIT_PATTERN =
Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
static final char SNIPPET_START_MATCH = '[';
static final char SNIPPET_END_MATCH = ']';
/** A helper used to highlight a prefix in a text field. */
private final TextHighlighter mTextHighlighter;
// Style values for layout and appearance
// The initialized values are defaults if none is provided through xml.
private int mPreferredHeight = 0;
private int mGapBetweenImageAndText = 0;
private int mGapBetweenLabelAndData = 0;
private int mPresenceIconMargin = 4;
private int mPresenceIconSize = 16;
private int mTextIndent = 0;
private int mTextOffsetTop;
private int mNameTextViewTextSize;
private int mHeaderWidth;
private Drawable mActivatedBackgroundDrawable;
private int mCallToActionSize = 32;
private int mCallToActionMargin = 16;
// Set in onLayout. Represent left and right position of the View on the screen.
private int mLeftOffset;
private int mRightOffset;
/** Used with {@link #mLabelView}, specifying the width ratio between label and data. */
private int mLabelViewWidthWeight = 3;
/** Used with {@link #mDataView}, specifying the width ratio between label and data. */
private int mDataViewWidthWeight = 5;
private ArrayList mNameHighlightSequence;
private ArrayList mNumberHighlightSequence;
// Highlighting prefix for names.
private String mHighlightedPrefix;
/** Indicates whether the view should leave room for the "video call" icon. */
private boolean mSupportVideoCall;
/** Indicates whether the view should leave room for the "call and share" icon. */
private boolean mSupportCallAndShare;
private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */);
// Header layout data
private TextView mHeaderTextView;
private boolean mIsSectionHeaderEnabled;
// The views inside the contact view
private boolean mQuickContactEnabled = true;
private QuickContactBadge mQuickContact;
private ImageView mPhotoView;
private TextView mNameTextView;
private TextView mLabelView;
private TextView mDataView;
private TextView mSnippetView;
private TextView mStatusView;
private ImageView mPresenceIcon;
private ImageView mCallToAction;
private ImageView mWorkProfileIcon;
private ColorStateList mSecondaryTextColor;
private int mDefaultPhotoViewSize = 0;
/**
* Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
* to align other data in this View.
*/
private int mPhotoViewWidth;
/**
* Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
*/
private int mPhotoViewHeight;
/**
* Only effective when {@link #mPhotoView} is null. When true all the Views on the right side of
* the photo should have horizontal padding on those left assuming there is a photo.
*/
private boolean mKeepHorizontalPaddingForPhotoView;
/** Only effective when {@link #mPhotoView} is null. */
private boolean mKeepVerticalPaddingForPhotoView;
/**
* True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
* False indicates those values should be updated before being used in position calculation.
*/
private boolean mPhotoViewWidthAndHeightAreReady = false;
private int mNameTextViewHeight;
private int mNameTextViewTextColor = Color.BLACK;
private int mPhoneticNameTextViewHeight;
private int mLabelViewHeight;
private int mDataViewHeight;
private int mSnippetTextViewHeight;
private int mStatusTextViewHeight;
private int mCheckBoxWidth;
// Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
// same row.
private int mLabelAndDataViewMaxHeight;
private boolean mActivatedStateSupported;
private boolean mAdjustSelectionBoundsEnabled = true;
private Rect mBoundsWithoutHeader = new Rect();
private CharSequence mUnknownNameText;
public ContactListItemView(Context context) {
super(context);
mTextHighlighter = new TextHighlighter(Typeface.BOLD);
mNameHighlightSequence = new ArrayList<>();
mNumberHighlightSequence = new ArrayList<>();
}
public ContactListItemView(
Context context,
AttributeSet attrs,
boolean supportVideoCallIcon,
boolean supportCallAndShare) {
this(context, attrs);
mSupportVideoCall = supportVideoCallIcon;
mSupportCallAndShare = supportCallAndShare;
}
public ContactListItemView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a;
if (R.styleable.ContactListItemView != null) {
// Read all style values
a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
mPreferredHeight =
a.getDimensionPixelSize(
R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
mActivatedBackgroundDrawable =
a.getDrawable(R.styleable.ContactListItemView_activated_background);
mGapBetweenImageAndText =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
mGapBetweenImageAndText);
mGapBetweenLabelAndData =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
mGapBetweenLabelAndData);
mPresenceIconMargin =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_presence_icon_margin, mPresenceIconMargin);
mPresenceIconSize =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize);
mDefaultPhotoViewSize =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
mTextIndent =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
mTextOffsetTop =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop);
mDataViewWidthWeight =
a.getInteger(
R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight);
mLabelViewWidthWeight =
a.getInteger(
R.styleable.ContactListItemView_list_item_label_width_weight, mLabelViewWidthWeight);
mNameTextViewTextColor =
a.getColor(
R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor);
mNameTextViewTextSize =
(int)
a.getDimension(
R.styleable.ContactListItemView_list_item_name_text_size,
(int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
mCallToActionSize =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_video_call_icon_size, mCallToActionSize);
mCallToActionMargin =
a.getDimensionPixelOffset(
R.styleable.ContactListItemView_list_item_video_call_icon_margin,
mCallToActionMargin);
setPaddingRelative(
a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_left, 0),
a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_top, 0),
a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_right, 0),
a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_bottom, 0));
a.recycle();
}
mTextHighlighter = new TextHighlighter(Typeface.BOLD);
if (R.styleable.Theme != null) {
a = getContext().obtainStyledAttributes(R.styleable.Theme);
mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary);
a.recycle();
}
mHeaderWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
if (mActivatedBackgroundDrawable != null) {
mActivatedBackgroundDrawable.setCallback(this);
}
mNameHighlightSequence = new ArrayList<>();
mNumberHighlightSequence = new ArrayList<>();
setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
}
public static PhotoPosition getDefaultPhotoPosition(boolean opposite) {
final Locale locale = Locale.getDefault();
final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
switch (layoutDirection) {
case View.LAYOUT_DIRECTION_RTL:
return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT);
case View.LAYOUT_DIRECTION_LTR:
default:
return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT);
}
}
/**
* Helper method for splitting a string into tokens. The lists passed in are populated with the
* tokens and offsets into the content of each token. The tokenization function parses e-mail
* addresses as a single token; otherwise it splits on any non-alphanumeric character.
*
* @param content Content to split.
* @return List of token strings.
*/
private static List split(String content) {
final Matcher matcher = SPLIT_PATTERN.matcher(content);
final ArrayList tokens = new ArrayList<>();
while (matcher.find()) {
tokens.add(matcher.group());
}
return tokens;
}
public void setUnknownNameText(CharSequence unknownNameText) {
mUnknownNameText = unknownNameText;
}
public void setQuickContactEnabled(boolean flag) {
mQuickContactEnabled = flag;
}
/**
* Sets whether the call to action is shown. For the {@link CallToAction} to be shown, it must be
* supported as well.
*
* @param action {@link CallToAction} you want to display (if it's supported).
* @param listener Listener to notify when the call to action is clicked.
* @param position The position in the adapter of the call to action.
*/
public void setCallToAction(@CallToAction int action, Listener listener, int position) {
int drawable;
int description;
OnClickListener onClickListener;
if (action == CALL_AND_SHARE && mSupportCallAndShare) {
drawable = R.drawable.ic_call_and_share;
description = R.string.description_search_video_call;
onClickListener = v -> listener.onCallAndShareIconClicked(position);
} else if (action == VIDEO && mSupportVideoCall) {
drawable = R.drawable.ic_search_video_call;
description = R.string.description_search_call_and_share;
onClickListener = v -> listener.onVideoCallIconClicked(position);
} else {
if (mCallToAction != null) {
mCallToAction.setVisibility(View.GONE);
mCallToAction.setOnClickListener(null);
}
return;
}
if (mCallToAction == null) {
mCallToAction = new ImageView(getContext());
mCallToAction.setLayoutParams(new LayoutParams(mCallToActionSize, mCallToActionSize));
mCallToAction.setScaleType(ScaleType.CENTER);
addView(mCallToAction);
}
mCallToAction.setContentDescription(getContext().getString(description));
mCallToAction.setOnClickListener(onClickListener);
mCallToAction.setImageResource(drawable);
mCallToAction.setVisibility(View.VISIBLE);
}
/**
* Sets whether the view supports a video calling icon. This is independent of whether the view is
* actually showing an icon. Support for the video calling icon ensures that the layout leaves
* space for the video icon, should it be shown.
*
* @param supportVideoCall {@code true} if the video call icon is supported, {@code false}
* otherwise.
*/
public void setSupportVideoCallIcon(boolean supportVideoCall) {
mSupportVideoCall = supportVideoCall;
}
/**
* Sets whether the view supports a call and share icon. This is independent of whether the view
* is actually showing an icon. Support for the icon ensures that the layout leaves space for it,
* should it be shown.
*
* @param supportCallAndShare {@code true} if the call and share icon is supported, {@code false}
* otherwise.
*/
public void setSupportCallAndShareIcon(boolean supportCallAndShare) {
mSupportCallAndShare = supportCallAndShare;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// We will match parent's width and wrap content vertically, but make sure
// height is no less than listPreferredItemHeight.
final int specWidth = resolveSize(0, widthMeasureSpec);
final int preferredHeight = mPreferredHeight;
mNameTextViewHeight = 0;
mPhoneticNameTextViewHeight = 0;
mLabelViewHeight = 0;
mDataViewHeight = 0;
mLabelAndDataViewMaxHeight = 0;
mSnippetTextViewHeight = 0;
mStatusTextViewHeight = 0;
mCheckBoxWidth = 0;
ensurePhotoViewSize();
// Width each TextView is able to use.
int effectiveWidth;
// All the other Views will honor the photo, so available width for them may be shrunk.
if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
effectiveWidth =
specWidth
- getPaddingLeft()
- getPaddingRight()
- (mPhotoViewWidth + mGapBetweenImageAndText);
} else {
effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
}
if (mIsSectionHeaderEnabled) {
effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText;
}
if (mSupportVideoCall || mSupportCallAndShare) {
effectiveWidth -= (mCallToActionSize + mCallToActionMargin);
}
// Go over all visible text views and measure actual width of each of them.
// Also calculate their heights to get the total height for this entire view.
if (isVisible(mNameTextView)) {
// Calculate width for name text - this parallels similar measurement in onLayout.
int nameTextWidth = effectiveWidth;
if (mPhotoPosition != PhotoPosition.LEFT) {
nameTextWidth -= mTextIndent;
}
mNameTextView.measure(
MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mNameTextViewHeight = mNameTextView.getMeasuredHeight();
}
// If both data (phone number/email address) and label (type like "MOBILE") are quite long,
// we should ellipsize both using appropriate ratio.
final int dataWidth;
final int labelWidth;
if (isVisible(mDataView)) {
if (isVisible(mLabelView)) {
final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
dataWidth =
((totalWidth * mDataViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
labelWidth =
((totalWidth * mLabelViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
} else {
dataWidth = effectiveWidth;
labelWidth = 0;
}
} else {
dataWidth = 0;
if (isVisible(mLabelView)) {
labelWidth = effectiveWidth;
} else {
labelWidth = 0;
}
}
if (isVisible(mDataView)) {
mDataView.measure(
MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mDataViewHeight = mDataView.getMeasuredHeight();
}
if (isVisible(mLabelView)) {
mLabelView.measure(
MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mLabelViewHeight = mLabelView.getMeasuredHeight();
}
mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
if (isVisible(mSnippetView)) {
mSnippetView.measure(
MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
}
// Status view height is the biggest of the text view and the presence icon
if (isVisible(mPresenceIcon)) {
mPresenceIcon.measure(
MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
}
if ((mSupportVideoCall || mSupportCallAndShare) && isVisible(mCallToAction)) {
mCallToAction.measure(
MeasureSpec.makeMeasureSpec(mCallToActionSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCallToActionSize, MeasureSpec.EXACTLY));
}
if (isVisible(mWorkProfileIcon)) {
mWorkProfileIcon.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mNameTextViewHeight = Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight());
}
if (isVisible(mStatusView)) {
// Presence and status are in a same row, so status will be affected by icon size.
final int statusWidth;
if (isVisible(mPresenceIcon)) {
statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() - mPresenceIconMargin);
} else {
statusWidth = effectiveWidth;
}
mStatusView.measure(
MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
mStatusTextViewHeight = Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
}
// Calculate height including padding.
int height =
(mNameTextViewHeight
+ mPhoneticNameTextViewHeight
+ mLabelAndDataViewMaxHeight
+ mSnippetTextViewHeight
+ mStatusTextViewHeight);
// Make sure the height is at least as high as the photo
height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
// Make sure height is at least the preferred height
height = Math.max(height, preferredHeight);
// Measure the header if it is visible.
if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) {
mHeaderTextView.measure(
MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
}
setMeasuredDimension(specWidth, height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int height = bottom - top;
final int width = right - left;
// Determine the vertical bounds by laying out the header first.
int topBound = 0;
int leftBound = getPaddingLeft();
int rightBound = width - getPaddingRight();
final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);
// Put the section header on the left side of the contact view.
if (mIsSectionHeaderEnabled) {
// Align the text view all the way left, to be consistent with Contacts.
if (isLayoutRtl) {
rightBound = width;
} else {
leftBound = 0;
}
if (mHeaderTextView != null) {
int headerHeight = mHeaderTextView.getMeasuredHeight();
int headerTopBound = (height + topBound - headerHeight) / 2 + mTextOffsetTop;
mHeaderTextView.layout(
isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
headerTopBound,
isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
headerTopBound + headerHeight);
}
if (isLayoutRtl) {
rightBound -= mHeaderWidth;
} else {
leftBound += mHeaderWidth;
}
}
mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, height);
mLeftOffset = left + leftBound;
mRightOffset = left + rightBound;
if (mIsSectionHeaderEnabled) {
if (isLayoutRtl) {
rightBound -= mGapBetweenImageAndText;
} else {
leftBound += mGapBetweenImageAndText;
}
}
if (mActivatedStateSupported && isActivated()) {
mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
}
final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
if (mPhotoPosition == PhotoPosition.LEFT) {
// Photo is the left most view. All the other Views should on the right of the photo.
if (photoView != null) {
// Center the photo vertically
final int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2;
photoView.layout(
leftBound, photoTop, leftBound + mPhotoViewWidth, photoTop + mPhotoViewHeight);
leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
} else if (mKeepHorizontalPaddingForPhotoView) {
// Draw nothing but keep the padding.
leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
}
} else {
// Photo is the right most view. Right bound should be adjusted that way.
if (photoView != null) {
// Center the photo vertically
final int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2;
photoView.layout(
rightBound - mPhotoViewWidth, photoTop, rightBound, photoTop + mPhotoViewHeight);
rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
} else if (mKeepHorizontalPaddingForPhotoView) {
// Draw nothing but keep the padding.
rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
}
// Add indent between left-most padding and texts.
leftBound += mTextIndent;
}
if (mSupportVideoCall || mSupportCallAndShare) {
// Place the call to action at the end of the list (e.g. take into account RTL mode).
if (isVisible(mCallToAction)) {
// Center the icon vertically
final int callToActionTop = topBound + (height - topBound - mCallToActionSize) / 2;
if (!isLayoutRtl) {
// When photo is on left, icon is placed on the right edge.
mCallToAction.layout(
rightBound - mCallToActionSize,
callToActionTop,
rightBound,
callToActionTop + mCallToActionSize);
} else {
// When photo is on right, icon is placed on the left edge.
mCallToAction.layout(
leftBound,
callToActionTop,
leftBound + mCallToActionSize,
callToActionTop + mCallToActionSize);
}
}
if (mPhotoPosition == PhotoPosition.LEFT) {
rightBound -= (mCallToActionSize + mCallToActionMargin);
} else {
leftBound += mCallToActionSize + mCallToActionMargin;
}
}
// Center text vertically, then apply the top offset.
final int totalTextHeight =
mNameTextViewHeight
+ mPhoneticNameTextViewHeight
+ mLabelAndDataViewMaxHeight
+ mSnippetTextViewHeight
+ mStatusTextViewHeight;
int textTopBound = (height + topBound - totalTextHeight) / 2 + mTextOffsetTop;
// Work Profile icon align top
int workProfileIconWidth = 0;
if (isVisible(mWorkProfileIcon)) {
workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
final int distanceFromEnd = mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
if (mPhotoPosition == PhotoPosition.LEFT) {
// When photo is on left, label is placed on the right edge of the list item.
mWorkProfileIcon.layout(
rightBound - workProfileIconWidth - distanceFromEnd,
textTopBound,
rightBound - distanceFromEnd,
textTopBound + mNameTextViewHeight);
} else {
// When photo is on right, label is placed on the left of data view.
mWorkProfileIcon.layout(
leftBound + distanceFromEnd,
textTopBound,
leftBound + workProfileIconWidth + distanceFromEnd,
textTopBound + mNameTextViewHeight);
}
}
// Layout all text view and presence icon
// Put name TextView first
if (isVisible(mNameTextView)) {
final int distanceFromEnd =
workProfileIconWidth
+ (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
if (mPhotoPosition == PhotoPosition.LEFT) {
mNameTextView.layout(
leftBound,
textTopBound,
rightBound - distanceFromEnd,
textTopBound + mNameTextViewHeight);
} else {
mNameTextView.layout(
leftBound + distanceFromEnd,
textTopBound,
rightBound,
textTopBound + mNameTextViewHeight);
}
}
if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
textTopBound += mNameTextViewHeight;
}
// Presence and status
if (isLayoutRtl) {
int statusRightBound = rightBound;
if (isVisible(mPresenceIcon)) {
int iconWidth = mPresenceIcon.getMeasuredWidth();
mPresenceIcon.layout(
rightBound - iconWidth, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
statusRightBound -= (iconWidth + mPresenceIconMargin);
}
if (isVisible(mStatusView)) {
mStatusView.layout(
leftBound, textTopBound, statusRightBound, textTopBound + mStatusTextViewHeight);
}
} else {
int statusLeftBound = leftBound;
if (isVisible(mPresenceIcon)) {
int iconWidth = mPresenceIcon.getMeasuredWidth();
mPresenceIcon.layout(
leftBound, textTopBound, leftBound + iconWidth, textTopBound + mStatusTextViewHeight);
statusLeftBound += (iconWidth + mPresenceIconMargin);
}
if (isVisible(mStatusView)) {
mStatusView.layout(
statusLeftBound, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
}
}
if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
textTopBound += mStatusTextViewHeight;
}
// Rest of text views
int dataLeftBound = leftBound;
// Label and Data align bottom.
if (isVisible(mLabelView)) {
if (!isLayoutRtl) {
mLabelView.layout(
dataLeftBound,
textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
rightBound,
textTopBound + mLabelAndDataViewMaxHeight);
dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
} else {
dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
mLabelView.layout(
rightBound - mLabelView.getMeasuredWidth(),
textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
rightBound,
textTopBound + mLabelAndDataViewMaxHeight);
rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
}
}
if (isVisible(mDataView)) {
if (!isLayoutRtl) {
mDataView.layout(
dataLeftBound,
textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
rightBound,
textTopBound + mLabelAndDataViewMaxHeight);
} else {
mDataView.layout(
rightBound - mDataView.getMeasuredWidth(),
textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
rightBound,
textTopBound + mLabelAndDataViewMaxHeight);
}
}
if (isVisible(mLabelView) || isVisible(mDataView)) {
textTopBound += mLabelAndDataViewMaxHeight;
}
if (isVisible(mSnippetView)) {
mSnippetView.layout(
leftBound, textTopBound, rightBound, textTopBound + mSnippetTextViewHeight);
}
}
@Override
public void adjustListItemSelectionBounds(Rect bounds) {
if (mAdjustSelectionBoundsEnabled) {
bounds.top += mBoundsWithoutHeader.top;
bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
bounds.left = mBoundsWithoutHeader.left;
bounds.right = mBoundsWithoutHeader.right;
}
}
protected boolean isVisible(View view) {
return view != null && view.getVisibility() == View.VISIBLE;
}
/** Extracts width and height from the style */
private void ensurePhotoViewSize() {
if (!mPhotoViewWidthAndHeightAreReady) {
mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
if (!mQuickContactEnabled && mPhotoView == null) {
if (!mKeepHorizontalPaddingForPhotoView) {
mPhotoViewWidth = 0;
}
if (!mKeepVerticalPaddingForPhotoView) {
mPhotoViewHeight = 0;
}
}
mPhotoViewWidthAndHeightAreReady = true;
}
}
protected int getDefaultPhotoViewSize() {
return mDefaultPhotoViewSize;
}
/**
* Gets a LayoutParam that corresponds to the default photo size.
*
* @return A new LayoutParam.
*/
private LayoutParams getDefaultPhotoLayoutParams() {
LayoutParams params = generateDefaultLayoutParams();
params.width = getDefaultPhotoViewSize();
params.height = params.width;
return params;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mActivatedStateSupported) {
mActivatedBackgroundDrawable.setState(getDrawableState());
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mActivatedStateSupported) {
mActivatedBackgroundDrawable.jumpToCurrentState();
}
}
@Override
public void dispatchDraw(Canvas canvas) {
if (mActivatedStateSupported && isActivated()) {
mActivatedBackgroundDrawable.draw(canvas);
}
super.dispatchDraw(canvas);
}
/** Sets section header or makes it invisible if the title is null. */
public void setSectionHeader(String title) {
if (!TextUtils.isEmpty(title)) {
if (mHeaderTextView == null) {
mHeaderTextView = new TextView(getContext());
mHeaderTextView.setTextAppearance(R.style.SectionHeaderStyle);
mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
addView(mHeaderTextView);
}
setMarqueeText(mHeaderTextView, title);
mHeaderTextView.setVisibility(View.VISIBLE);
mHeaderTextView.setAllCaps(true);
} else if (mHeaderTextView != null) {
mHeaderTextView.setVisibility(View.GONE);
}
}
public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
mIsSectionHeaderEnabled = isSectionHeaderEnabled;
}
/** Returns the quick contact badge, creating it if necessary. */
public QuickContactBadge getQuickContact() {
if (!mQuickContactEnabled) {
throw new IllegalStateException("QuickContact is disabled for this view");
}
if (mQuickContact == null) {
mQuickContact = new QuickContactBadge(getContext());
if (CompatUtils.isLollipopCompatible()) {
mQuickContact.setOverlay(null);
}
mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
if (mNameTextView != null) {
mQuickContact.setContentDescription(
getContext()
.getString(R.string.description_quick_contact_for, mNameTextView.getText()));
}
addView(mQuickContact);
mPhotoViewWidthAndHeightAreReady = false;
}
return mQuickContact;
}
/** Returns the photo view, creating it if necessary. */
public ImageView getPhotoView() {
if (mPhotoView == null) {
mPhotoView = new ImageView(getContext());
mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
// Quick contact style used above will set a background - remove it
mPhotoView.setBackground(null);
addView(mPhotoView);
mPhotoViewWidthAndHeightAreReady = false;
}
return mPhotoView;
}
/** Removes the photo view. */
public void removePhotoView() {
removePhotoView(false, true);
}
/**
* Removes the photo view.
*
* @param keepHorizontalPadding True means data on the right side will have padding on left,
* pretending there is still a photo view.
* @param keepVerticalPadding True means the View will have some height enough for accommodating a
* photo view.
*/
public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
mPhotoViewWidthAndHeightAreReady = false;
mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
if (mPhotoView != null) {
removeView(mPhotoView);
mPhotoView = null;
}
if (mQuickContact != null) {
removeView(mQuickContact);
mQuickContact = null;
}
}
/**
* Sets a word prefix that will be highlighted if encountered in fields like name and search
* snippet. This will disable the mask highlighting for names.
*
* NOTE: must be all upper-case
*/
public void setHighlightedPrefix(String upperCasePrefix) {
mHighlightedPrefix = upperCasePrefix;
}
/** Clears previously set highlight sequences for the view. */
public void clearHighlightSequences() {
mNameHighlightSequence.clear();
mNumberHighlightSequence.clear();
mHighlightedPrefix = null;
}
/**
* Adds a highlight sequence to the name highlighter.
*
* @param start The start position of the highlight sequence.
* @param end The end position of the highlight sequence.
*/
public void addNameHighlightSequence(int start, int end) {
mNameHighlightSequence.add(new HighlightSequence(start, end));
}
/**
* Adds a highlight sequence to the number highlighter.
*
* @param start The start position of the highlight sequence.
* @param end The end position of the highlight sequence.
*/
public void addNumberHighlightSequence(int start, int end) {
mNumberHighlightSequence.add(new HighlightSequence(start, end));
}
/** Returns the text view for the contact name, creating it if necessary. */
public TextView getNameTextView() {
if (mNameTextView == null) {
mNameTextView = new TextView(getContext());
mNameTextView.setSingleLine(true);
mNameTextView.setEllipsize(getTextEllipsis());
mNameTextView.setTextColor(mNameTextViewTextColor);
mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
// Manually call setActivated() since this view may be added after the first
// setActivated() call toward this whole item view.
mNameTextView.setActivated(isActivated());
mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
mNameTextView.setId(R.id.cliv_name_textview);
if (CompatUtils.isLollipopCompatible()) {
mNameTextView.setElegantTextHeight(false);
}
addView(mNameTextView);
}
return mNameTextView;
}
/** Adds or updates a text view for the data label. */
public void setLabel(CharSequence text) {
if (TextUtils.isEmpty(text)) {
if (mLabelView != null) {
mLabelView.setVisibility(View.GONE);
}
} else {
getLabelView();
setMarqueeText(mLabelView, text);
mLabelView.setVisibility(VISIBLE);
}
}
/** Returns the text view for the data label, creating it if necessary. */
public TextView getLabelView() {
if (mLabelView == null) {
mLabelView = new TextView(getContext());
mLabelView.setLayoutParams(
new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
mLabelView.setSingleLine(true);
mLabelView.setEllipsize(getTextEllipsis());
mLabelView.setTextAppearance(R.style.TextAppearanceSmall);
if (mPhotoPosition == PhotoPosition.LEFT) {
mLabelView.setAllCaps(true);
} else {
mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
}
mLabelView.setActivated(isActivated());
mLabelView.setId(R.id.cliv_label_textview);
addView(mLabelView);
}
return mLabelView;
}
/**
* Sets phone number for a list item. This takes care of number highlighting if the highlight mask
* exists.
*/
public void setPhoneNumber(String text) {
if (text == null) {
if (mDataView != null) {
mDataView.setVisibility(View.GONE);
}
} else {
getDataView();
// TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
// mDataView. Make sure that determination of the highlight sequences are done only
// after number formatting.
// Sets phone number texts for display after highlighting it, if applicable.
// CharSequence textToSet = text;
final SpannableString textToSet = new SpannableString(text);
if (mNumberHighlightSequence.size() != 0) {
final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
mTextHighlighter.applyMaskingHighlight(
textToSet, highlightSequence.start, highlightSequence.end);
}
setMarqueeText(mDataView, textToSet);
mDataView.setVisibility(VISIBLE);
// We have a phone number as "mDataView" so make it always LTR and VIEW_START
mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
}
}
private void setMarqueeText(TextView textView, CharSequence text) {
if (getTextEllipsis() == TruncateAt.MARQUEE) {
// To show MARQUEE correctly (with END effect during non-active state), we need
// to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
final SpannableString spannable = new SpannableString(text);
spannable.setSpan(
TruncateAt.MARQUEE, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannable);
} else {
textView.setText(text);
}
}
/** Returns the text view for the data text, creating it if necessary. */
public TextView getDataView() {
if (mDataView == null) {
mDataView = new TextView(getContext());
mDataView.setSingleLine(true);
mDataView.setEllipsize(getTextEllipsis());
mDataView.setTextAppearance(R.style.TextAppearanceSmall);
mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
mDataView.setActivated(isActivated());
mDataView.setId(R.id.cliv_data_view);
if (CompatUtils.isLollipopCompatible()) {
mDataView.setElegantTextHeight(false);
}
addView(mDataView);
}
return mDataView;
}
/** Adds or updates a text view for the search snippet. */
public void setSnippet(String text) {
if (TextUtils.isEmpty(text)) {
if (mSnippetView != null) {
mSnippetView.setVisibility(View.GONE);
}
} else {
mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
mSnippetView.setVisibility(VISIBLE);
if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
// Give the text-to-speech engine a hint that it's a phone number
mSnippetView.setContentDescription(PhoneNumberUtilsCompat.createTtsSpannable(text));
} else {
mSnippetView.setContentDescription(null);
}
}
}
/** Returns the text view for the search snippet, creating it if necessary. */
public TextView getSnippetView() {
if (mSnippetView == null) {
mSnippetView = new TextView(getContext());
mSnippetView.setSingleLine(true);
mSnippetView.setEllipsize(getTextEllipsis());
mSnippetView.setTextAppearance(android.R.style.TextAppearance_Small);
mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
mSnippetView.setActivated(isActivated());
addView(mSnippetView);
}
return mSnippetView;
}
/** Returns the text view for the status, creating it if necessary. */
public TextView getStatusView() {
if (mStatusView == null) {
mStatusView = new TextView(getContext());
mStatusView.setSingleLine(true);
mStatusView.setEllipsize(getTextEllipsis());
mStatusView.setTextAppearance(android.R.style.TextAppearance_Small);
mStatusView.setTextColor(mSecondaryTextColor);
mStatusView.setActivated(isActivated());
mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
addView(mStatusView);
}
return mStatusView;
}
/** Adds or updates a text view for the status. */
public void setStatus(CharSequence text) {
if (TextUtils.isEmpty(text)) {
if (mStatusView != null) {
mStatusView.setVisibility(View.GONE);
}
} else {
getStatusView();
setMarqueeText(mStatusView, text);
mStatusView.setVisibility(VISIBLE);
}
}
/** Adds or updates the presence icon view. */
public void setPresence(Drawable icon) {
if (icon != null) {
if (mPresenceIcon == null) {
mPresenceIcon = new ImageView(getContext());
addView(mPresenceIcon);
}
mPresenceIcon.setImageDrawable(icon);
mPresenceIcon.setScaleType(ScaleType.CENTER);
mPresenceIcon.setVisibility(View.VISIBLE);
} else {
if (mPresenceIcon != null) {
mPresenceIcon.setVisibility(View.GONE);
}
}
}
/**
* Set to display work profile icon or not
*
* @param enabled set to display work profile icon or not
*/
public void setWorkProfileIconEnabled(boolean enabled) {
if (mWorkProfileIcon != null) {
mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
} else if (enabled) {
mWorkProfileIcon = new ImageView(getContext());
addView(mWorkProfileIcon);
mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
mWorkProfileIcon.setVisibility(View.VISIBLE);
}
}
private TruncateAt getTextEllipsis() {
return TruncateAt.MARQUEE;
}
public void showDisplayName(Cursor cursor, int nameColumnIndex) {
CharSequence name = cursor.getString(nameColumnIndex);
setDisplayName(name);
// Since the quick contact content description is derived from the display name and there is
// no guarantee that when the quick contact is initialized the display name is already set,
// do it here too.
if (mQuickContact != null) {
mQuickContact.setContentDescription(
getContext().getString(R.string.description_quick_contact_for, mNameTextView.getText()));
}
}
public void setDisplayName(CharSequence name) {
if (!TextUtils.isEmpty(name)) {
// Chooses the available highlighting method for highlighting.
if (mHighlightedPrefix != null) {
name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
} else if (mNameHighlightSequence.size() != 0) {
final SpannableString spannableName = new SpannableString(name);
for (HighlightSequence highlightSequence : mNameHighlightSequence) {
mTextHighlighter.applyMaskingHighlight(
spannableName, highlightSequence.start, highlightSequence.end);
}
name = spannableName;
}
} else {
name = mUnknownNameText;
}
setMarqueeText(getNameTextView(), name);
if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
// Give the text-to-speech engine a hint that it's a phone number
mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
mNameTextView.setContentDescription(
PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
} else {
// Remove span tags of highlighting for talkback to avoid reading highlighting and rest
// of the name into two separate parts.
mNameTextView.setContentDescription(name.toString());
}
}
public void hideDisplayName() {
if (mNameTextView != null) {
removeView(mNameTextView);
mNameTextView = null;
}
}
/** Sets the proper icon (star or presence or nothing) and/or status message. */
public void showPresenceAndStatusMessage(
Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex) {
Drawable icon = null;
int presence = 0;
if (!cursor.isNull(presenceColumnIndex)) {
presence = cursor.getInt(presenceColumnIndex);
icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
}
setPresence(icon);
String statusMessage = null;
if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
statusMessage = cursor.getString(contactStatusColumnIndex);
}
// If there is no status message from the contact, but there was a presence value, then use
// the default status message string
if (statusMessage == null && presence != 0) {
statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
}
setStatus(statusMessage);
}
/** Shows search snippet. */
public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
if (cursor.getColumnCount() <= summarySnippetColumnIndex
|| !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
setSnippet(null);
return;
}
String snippet = cursor.getString(summarySnippetColumnIndex);
// Do client side snippeting if provider didn't do it
final Bundle extras = cursor.getExtras();
if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
String displayName = null;
int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
if (displayNameIndex >= 0) {
displayName = cursor.getString(displayNameIndex);
}
snippet = updateSnippet(snippet, query, displayName);
} else {
if (snippet != null) {
int from = 0;
int to = snippet.length();
int start = snippet.indexOf(SNIPPET_START_MATCH);
if (start == -1) {
snippet = null;
} else {
int firstNl = snippet.lastIndexOf('\n', start);
if (firstNl != -1) {
from = firstNl + 1;
}
int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
if (end != -1) {
int lastNl = snippet.indexOf('\n', end);
if (lastNl != -1) {
to = lastNl;
}
}
StringBuilder sb = new StringBuilder();
for (int i = from; i < to; i++) {
char c = snippet.charAt(i);
if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
sb.append(c);
}
}
snippet = sb.toString();
}
}
}
setSnippet(snippet);
}
/**
* Used for deferred snippets from the database. The contents come back as large strings which
* need to be extracted for display.
*
* @param snippet The snippet from the database.
* @param query The search query substring.
* @param displayName The contact display name.
* @return The proper snippet to display.
*/
private String updateSnippet(String snippet, String query, String displayName) {
if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
return null;
}
query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
// If the display name already contains the query term, return empty - snippets should
// not be needed in that case.
if (!TextUtils.isEmpty(displayName)) {
final String lowerDisplayName = displayName.toLowerCase();
final List nameTokens = split(lowerDisplayName);
for (String nameToken : nameTokens) {
if (nameToken.startsWith(query)) {
return null;
}
}
}
// The snippet may contain multiple data lines.
// Show the first line that matches the query.
final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
if (matched != null && matched.line != null) {
// Tokenize for long strings since the match may be at the end of it.
// Skip this part for short strings since the whole string will be displayed.
// Most contact strings are short so the snippetize method will be called infrequently.
final int lengthThreshold =
getResources().getInteger(R.integer.snippet_length_before_tokenize);
if (matched.line.length() > lengthThreshold) {
return snippetize(matched.line, matched.startIndex, lengthThreshold);
} else {
return matched.line;
}
}
// No match found.
return null;
}
private String snippetize(String line, int matchIndex, int maxLength) {
// Show up to maxLength characters. But we only show full tokens so show the last full token
// up to maxLength characters. So as many starting tokens as possible before trying ending
// tokens.
int remainingLength = maxLength;
int tempRemainingLength = remainingLength;
// Start the end token after the matched query.
int index = matchIndex;
int endTokenIndex = index;
// Find the match token first.
while (index < line.length()) {
if (!Character.isLetterOrDigit(line.charAt(index))) {
endTokenIndex = index;
remainingLength = tempRemainingLength;
break;
}
tempRemainingLength--;
index++;
}
// Find as much content before the match.
index = matchIndex - 1;
tempRemainingLength = remainingLength;
int startTokenIndex = matchIndex;
while (index > -1 && tempRemainingLength > 0) {
if (!Character.isLetterOrDigit(line.charAt(index))) {
startTokenIndex = index;
remainingLength = tempRemainingLength;
}
tempRemainingLength--;
index--;
}
index = endTokenIndex;
tempRemainingLength = remainingLength;
// Find remaining content at after match.
while (index < line.length() && tempRemainingLength > 0) {
if (!Character.isLetterOrDigit(line.charAt(index))) {
endTokenIndex = index;
}
tempRemainingLength--;
index++;
}
// Append ellipse if there is content before or after.
final StringBuilder sb = new StringBuilder();
if (startTokenIndex > 0) {
sb.append("...");
}
sb.append(line.substring(startTokenIndex, endTokenIndex));
if (endTokenIndex < line.length()) {
sb.append("...");
}
return sb.toString();
}
public void setActivatedStateSupported(boolean flag) {
this.mActivatedStateSupported = flag;
}
public void setAdjustSelectionBoundsEnabled(boolean enabled) {
mAdjustSelectionBoundsEnabled = enabled;
}
@Override
public void requestLayout() {
// We will assume that once measured this will not need to resize
// itself, so there is no need to pass the layout request to the parent
// view (ListView).
forceLayout();
}
public void setPhotoPosition(PhotoPosition photoPosition) {
mPhotoPosition = photoPosition;
}
/**
* Set drawable resources directly for the drawable resource of the photo view.
*
* @param drawableId Id of drawable resource.
*/
public void setDrawableResource(int drawableId) {
ImageView photo = getPhotoView();
photo.setScaleType(ImageView.ScaleType.CENTER);
final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
final int iconColor = ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
if (CompatUtils.isLollipopCompatible()) {
photo.setImageDrawable(drawable);
photo.setImageTintList(ColorStateList.valueOf(iconColor));
} else {
final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
DrawableCompat.setTint(drawableWrapper, iconColor);
photo.setImageDrawable(drawableWrapper);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
// If the touch event's coordinates are not within the view's header, then delegate
// to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
// and ignore the touch event.
if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
return super.onTouchEvent(event);
} else {
return true;
}
}
private boolean pointIsInView(float localX, float localY) {
return localX >= mLeftOffset
&& localX < mRightOffset
&& localY >= 0
&& localY < (getBottom() - getTop());
}
/**
* Where to put contact photo. This affects the other Views' layout or look-and-feel.
*
* TODO: replace enum with int constants
*/
public enum PhotoPosition {
LEFT,
RIGHT
}
protected static class HighlightSequence {
private final int start;
private final int end;
HighlightSequence(int start, int end) {
this.start = start;
this.end = end;
}
}
}