Java tutorial
/* * 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.mobileglobe.android.customdialer.common.list; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.database.CharArrayBuffer; 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.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.CheckBox; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.QuickContactBadge; import android.widget.TextView; import com.mobileglobe.android.customdialer.common.ContactPresenceIconUtil; import com.mobileglobe.android.customdialer.common.ContactStatusUtil; import com.mobileglobe.android.customdialer.R; import com.mobileglobe.android.customdialer.common.compat.CompatUtils; import com.mobileglobe.android.customdialer.common.compat.PhoneNumberUtilsCompat; import com.mobileglobe.android.customdialer.common.format.TextHighlighter; import com.mobileglobe.android.customdialer.common.list.ContactListAdapter.ExperimentQuery; import com.mobileglobe.android.customdialer.common.util.ContactDisplayUtils; import com.mobileglobe.android.customdialer.common.util.SearchUtil; import com.mobileglobe.android.customdialer.common.util.ViewUtil; import com.google.common.collect.Lists; 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 { private static final String TAG = "ContactListItemView"; // 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 mVideoCallIconSize = 32; private int mVideoCallIconMargin = 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; protected static class HighlightSequence { private final int start; private final int end; HighlightSequence(int start, int end) { this.start = start; this.end = end; } } private ArrayList<HighlightSequence> mNameHighlightSequence; private ArrayList<HighlightSequence> mNumberHighlightSequence; // Highlighting prefix for names. private String mHighlightedPrefix; /** * Used to notify listeners when a video call icon is clicked. */ private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener; /** * Indicates whether to show the "video call" icon, used to initiate a video call. */ private boolean mShowVideoCallIcon = false; /** * Indicates whether the view should leave room for the "video call" icon. */ private boolean mSupportVideoCallIcon = false; /** * 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 } static public final 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); } } 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 mPhoneticNameTextView; private TextView mLabelView; private TextView mDataView; private TextView mSnippetView; private TextView mStatusView; private ImageView mPresenceIcon; private CheckBox mCheckBox; private ImageView mVideoCallIcon; 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 mCheckBoxHeight; private int mCheckBoxWidth; // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the // same row. private int mLabelAndDataViewMaxHeight; // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is // more efficient for each case or in general, and simplify the whole implementation. // Note: if we're sure MARQUEE will be used every time, there's no reason to use // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to // TextView without any modification. private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128); private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128); private boolean mActivatedStateSupported; private boolean mAdjustSelectionBoundsEnabled = true; private Rect mBoundsWithoutHeader = new Rect(); /** A helper used to highlight a prefix in a text field. */ private final TextHighlighter mTextHighlighter; private CharSequence mUnknownNameText; private int mPosition; public ContactListItemView(Context context) { super(context); mTextHighlighter = new TextHighlighter(Typeface.BOLD); mNameHighlightSequence = new ArrayList<HighlightSequence>(); mNumberHighlightSequence = new ArrayList<HighlightSequence>(); } public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) { this(context, attrs); mSupportVideoCallIcon = supportVideoCallIcon; } 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)); mVideoCallIconSize = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_video_call_icon_size, mVideoCallIconSize); mVideoCallIconMargin = a.getDimensionPixelOffset( R.styleable.ContactListItemView_list_item_video_call_icon_margin, mVideoCallIconMargin); 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<HighlightSequence>(); mNumberHighlightSequence = new ArrayList<HighlightSequence>(); setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); } public void setUnknownNameText(CharSequence unknownNameText) { mUnknownNameText = unknownNameText; } public void setQuickContactEnabled(boolean flag) { mQuickContactEnabled = flag; } /** * Sets whether the video calling icon is shown. For the video calling icon to be shown, * {@link #mSupportVideoCallIcon} must be {@code true}. * * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false} * otherwise. * @param listener Listener to notify when the video calling icon is clicked. * @param position The position in the adapater of the video calling icon. */ public void setShowVideoCallIcon(boolean showVideoCallIcon, PhoneNumberListAdapter.Listener listener, int position) { mShowVideoCallIcon = showVideoCallIcon; mPhoneNumberListAdapterListener = listener; mPosition = position; if (mShowVideoCallIcon) { if (mVideoCallIcon == null) { mVideoCallIcon = new ImageView(getContext()); addView(mVideoCallIcon); } mVideoCallIcon.setContentDescription(getContext().getString(R.string.description_search_video_call)); mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call); mVideoCallIcon.setScaleType(ScaleType.CENTER); mVideoCallIcon.setVisibility(View.VISIBLE); mVideoCallIcon.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Inform the adapter that the video calling icon was clicked. if (mPhoneNumberListAdapterListener != null) { mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition); } } }); } else { if (mVideoCallIcon != null) { mVideoCallIcon.setVisibility(View.GONE); } } } /** * 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 supportVideoCallIcon {@code true} if the video call icon is supported, {@code false} * otherwise. */ public void setSupportVideoCallIcon(boolean supportVideoCallIcon) { mSupportVideoCallIcon = supportVideoCallIcon; } @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; mCheckBoxHeight = 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 (mSupportVideoCallIcon) { effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin); } // 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(mCheckBox)) { mCheckBox.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); mCheckBoxWidth = mCheckBox.getMeasuredWidth(); mCheckBoxHeight = mCheckBox.getMeasuredHeight(); effectiveWidth -= mCheckBoxWidth + mGapBetweenImageAndText; } 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 (isVisible(mPhoneticNameTextView)) { mPhoneticNameTextView.measure(MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); mPhoneticNameTextViewHeight = mPhoneticNameTextView.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 (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) { mVideoCallIcon.measure(MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mVideoCallIconSize, 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 bottomBound = height; 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) { if (mHeaderTextView != null) { int headerHeight = mHeaderTextView.getMeasuredHeight(); int headerTopBound = (bottomBound + 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, bottomBound); mLeftOffset = left + leftBound; mRightOffset = left + rightBound; if (mIsSectionHeaderEnabled) { if (isLayoutRtl) { rightBound -= mGapBetweenImageAndText; } else { leftBound += mGapBetweenImageAndText; } } if (mActivatedStateSupported && isActivated()) { mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); } if (isVisible(mCheckBox)) { final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2; if (mPhotoPosition == PhotoPosition.LEFT) { mCheckBox.layout(rightBound - mCheckBoxWidth, photoTop, rightBound, photoTop + mCheckBoxHeight); } else { mCheckBox.layout(leftBound, photoTop, leftBound + mCheckBoxWidth, photoTop + mCheckBoxHeight); } } 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 + (bottomBound - 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 + (bottomBound - 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 (mSupportVideoCallIcon) { // Place the video call button at the end of the list (e.g. take into account RTL mode). if (isVisible(mVideoCallIcon)) { // Center the video icon vertically final int videoIconTop = topBound + (bottomBound - topBound - mVideoCallIconSize) / 2; if (!isLayoutRtl) { // When photo is on left, video icon is placed on the right edge. mVideoCallIcon.layout(rightBound - mVideoCallIconSize, videoIconTop, rightBound, videoIconTop + mVideoCallIconSize); } else { // When photo is on right, video icon is placed on the left edge. mVideoCallIcon.layout(leftBound, videoIconTop, leftBound + mVideoCallIconSize, videoIconTop + mVideoCallIconSize); } } if (mPhotoPosition == PhotoPosition.LEFT) { rightBound -= (mVideoCallIconSize + mVideoCallIconMargin); } else { leftBound += mVideoCallIconSize + mVideoCallIconMargin; } } // Center text vertically, then apply the top offset. final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight + mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight; int textTopBound = (bottomBound + 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; if (isVisible(mPhoneticNameTextView)) { mPhoneticNameTextView.layout(leftBound, textTopBound, rightBound, textTopBound + mPhoneticNameTextViewHeight); textTopBound += mPhoneticNameTextViewHeight; } // 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(getContext(), R.style.SectionHeaderStyle); mHeaderTextView.setGravity(ViewUtil.isViewLayoutRtl(this) ? Gravity.RIGHT : Gravity.LEFT); 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. * <p> * 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 phonetic name. */ public void setPhoneticName(char[] text, int size) { if (text == null || size == 0) { if (mPhoneticNameTextView != null) { mPhoneticNameTextView.setVisibility(View.GONE); } } else { getPhoneticNameTextView(); setMarqueeText(mPhoneticNameTextView, text, size); mPhoneticNameTextView.setVisibility(VISIBLE); } } /** * Returns the text view for the phonetic name, creating it if necessary. */ public TextView getPhoneticNameTextView() { if (mPhoneticNameTextView == null) { mPhoneticNameTextView = new TextView(getContext()); mPhoneticNameTextView.setSingleLine(true); mPhoneticNameTextView.setEllipsize(getTextEllipsis()); mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD); mPhoneticNameTextView.setActivated(isActivated()); mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview); addView(mPhoneticNameTextView); } return mPhoneticNameTextView; } /** * 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(getContext(), 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; } /** * Adds or updates a text view for the data element. */ public void setData(char[] text, int size) { if (text == null || size == 0) { if (mDataView != null) { mDataView.setVisibility(View.GONE); } } else { getDataView(); setMarqueeText(mDataView, text, size); mDataView.setVisibility(VISIBLE); } } /** * Sets phone number for a list item. This takes care of number highlighting if the highlight * mask exists. */ public void setPhoneNumber(String text, String countryIso) { 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, char[] text, int size) { if (getTextEllipsis() == TruncateAt.MARQUEE) { setMarqueeText(textView, new String(text, 0, size)); } else { textView.setText(text, 0, size); } } 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 {@link CheckBox} view, creating it if necessary. */ public CheckBox getCheckBox() { if (mCheckBox == null) { mCheckBox = new CheckBox(getContext()); // Make non-focusable, so the rest of the ContactListItemView can be clicked. mCheckBox.setFocusable(false); addView(mCheckBox); } return mCheckBox; } /** * 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(getContext(), 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(getContext(), 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(getContext(), 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, int displayOrder) { 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, boolean highlight) { if (!TextUtils.isEmpty(name) && highlight) { clearHighlightSequences(); addNameHighlightSequence(0, name.length()); } setDisplayName(name); } 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 hideCheckBox() { if (mCheckBox != null) { removeView(mCheckBox); mCheckBox = null; } } public void hideDisplayName() { if (mNameTextView != null) { removeView(mNameTextView); mNameTextView = null; } } public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) { cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer); int phoneticNameSize = mPhoneticNameBuffer.sizeCopied; if (phoneticNameSize != 0) { setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize); } else { setPhoneticName(null, 0); } } public void hidePhoneticName() { if (mPhoneticNameTextView != null) { removeView(mPhoneticNameTextView); mPhoneticNameTextView = 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 for email and phone number matches. */ public void showSnippet(Cursor cursor, String query, int snippetColumn) { // TODO: this does not properly handle phone numbers with control characters // For example if the phone number is 444-5555, the search query 4445 will match the // number since we normalize it before querying CP2 but the snippet will fail since // the portion to be highlighted is 444-5 not 4445. final String snippet = cursor.getString(snippetColumn); if (snippet == null) { setSnippet(null); return; } final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0 ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null; if (snippet.equals(displayName)) { // If the snippet exactly matches the display name (i.e. the phone number or email // address is being used as the display name) then no snippet is necessary setSnippet(null); return; } // Show the snippet with the part of the query that matched it setSnippet(updateSnippet(snippet, query, displayName)); } /** * 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(DefaultContactListAdapter.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(DefaultContactListAdapter.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 != DefaultContactListAdapter.SNIPPET_START_MATCH && c != DefaultContactListAdapter.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<String> 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(); } private static final Pattern SPLIT_PATTERN = Pattern .compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); /** * 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<String> split(String content) { final Matcher matcher = SPLIT_PATTERN.matcher(content); final ArrayList<String> tokens = Lists.newArrayList(); while (matcher.find()) { tokens.add(matcher.group()); } return tokens; } /** * Shows data element. */ public void showData(Cursor cursor, int dataColumnIndex) { cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer); setData(mDataBuffer.data, mDataBuffer.sizeCopied); } 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; } public PhotoPosition getPhotoPosition() { return mPhotoPosition; } /** * 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(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 final boolean pointIsInView(float localX, float localY) { return localX >= mLeftOffset && localX < mRightOffset && localY >= 0 && localY < (getBottom() - getTop()); } }