FileDocCategorySizeDatePackage
RecipientEditTextView.javaAPI DocAndroid 5.1 API124788Thu Mar 12 22:22:50 GMT 2015com.android.ex.chips

RecipientEditTextView

public class RecipientEditTextView extends android.widget.MultiAutoCompleteTextView implements android.view.ActionMode.Callback, DropdownChipLayouter.ChipDeleteListener, android.widget.AdapterView.OnItemClickListener, GestureDetector.OnGestureListener, android.content.DialogInterface.OnDismissListener, android.view.View.OnClickListener, TextView.OnEditorActionListener, RecipientAlternatesAdapter.OnCheckedItemChangedListener
RecipientEditTextView is an auto complete text view for use with applications that use the new Chips UI for addressing a message to recipients.

Fields Summary
private static final String
TAG
private static final char
COMMIT_CHAR_COMMA
private static final char
COMMIT_CHAR_SEMICOLON
private static final char
COMMIT_CHAR_SPACE
private static final String
SEPARATOR
private static final Pattern
PHONE_PATTERN
private static final int
DISMISS
private static final long
DISMISS_DELAY
static final int
CHIP_LIMIT
private static final int
MAX_CHIPS_PARSED
private static int
sSelectedTextColor
private static int
sExcessTopPadding
private android.graphics.drawable.Drawable
mChipBackground
private android.graphics.drawable.Drawable
mChipDelete
private android.graphics.drawable.Drawable
mInvalidChipBackground
private android.graphics.drawable.Drawable
mChipBackgroundPressed
private float
mChipHeight
private float
mChipFontSize
private float
mLineSpacingExtra
private int
mChipTextStartPadding
private int
mChipTextEndPadding
private final int
mTextHeight
private boolean
mDisableDelete
private int
mMaxLines
private int
mActionBarHeight
private int
mAvatarPosition
Enumerator for avatar position. See attr.xml for more details. 0 for end, 1 for start.
private static final int
AVATAR_POSITION_END
private static final int
AVATAR_POSITION_START
private android.graphics.Paint
mWorkPaint
private Tokenizer
mTokenizer
private Validator
mValidator
private android.os.Handler
mHandler
private android.text.TextWatcher
mTextWatcher
private DropdownChipLayouter
mDropdownChipLayouter
private android.widget.ListPopupWindow
mAlternatesPopup
private android.widget.ListPopupWindow
mAddressPopup
private android.view.View
mAlternatePopupAnchor
private android.widget.AdapterView.OnItemClickListener
mAlternatesListener
private com.android.ex.chips.recipientchip.DrawableRecipientChip
mSelectedChip
private android.graphics.Bitmap
mDefaultContactPhoto
private com.android.ex.chips.recipientchip.ReplacementDrawableSpan
mMoreChip
private android.widget.TextView
mMoreItem
final ArrayList
mPendingChips
private int
mPendingChipsCount
private int
mCheckedItem
private boolean
mNoChips
private boolean
mShouldShrink
ArrayList
mTemporaryRecipients
private ArrayList
mRemovedSpans
private android.view.GestureDetector
mGestureDetector
private android.app.Dialog
mCopyDialog
private String
mCopyAddress
private android.widget.ScrollView
mScrollView
private boolean
mTriedGettingScrollView
private boolean
mDragEnabled
private boolean
mAttachedToWindow
private final Runnable
mAddTextWatcher
private IndividualReplacementTask
mIndividualReplacements
private Runnable
mHandlePendingChips
private Runnable
mDelayedShrink
private RecipientEntryItemClickedListener
mRecipientEntryItemClickedListener
Constructors Summary
public RecipientEditTextView(android.content.Context context, android.util.AttributeSet attrs)


       
                                                                            
            
    

         
        super(context, attrs);
        setChipDimensions(context, attrs);
        mTextHeight = calculateTextHeight();
        if (sSelectedTextColor == -1) {
            sSelectedTextColor = context.getResources().getColor(android.R.color.white);
        }
        mAlternatesPopup = new ListPopupWindow(context);
        mAlternatesPopup.setBackgroundDrawable(null);
        mAddressPopup = new ListPopupWindow(context);
        mAddressPopup.setBackgroundDrawable(null);
        mCopyDialog = new Dialog(context);
        mAlternatesListener = new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView,View view, int position,
                    long rowId) {
                mAlternatesPopup.setOnItemClickListener(null);
                replaceChip(mSelectedChip, ((RecipientAlternatesAdapter) adapterView.getAdapter())
                        .getRecipientEntry(position));
                Message delayed = Message.obtain(mHandler, DISMISS);
                delayed.obj = mAlternatesPopup;
                mHandler.sendMessageDelayed(delayed, DISMISS_DELAY);
                clearComposingText();
            }
        };
        setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
        setOnItemClickListener(this);
        setCustomSelectionActionModeCallback(this);
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == DISMISS) {
                    ((ListPopupWindow) msg.obj).dismiss();
                    return;
                }
                super.handleMessage(msg);
            }
        };
        mTextWatcher = new RecipientTextWatcher();
        addTextChangedListener(mTextWatcher);
        mGestureDetector = new GestureDetector(context, this);
        setOnEditorActionListener(this);

        setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context));
    
Methods Summary
private booleanalreadyHasChip(int start, int end)

        if (mNoChips) {
            return true;
        }
        DrawableRecipientChip[] chips =
                getSpannable().getSpans(start, end, DrawableRecipientChip.class);
        if ((chips == null || chips.length == 0)) {
            return false;
        }
        return true;
    
public voidappend(java.lang.CharSequence text, int start, int end)
Convenience method: Append the specified text slice to the TextView's display buffer, upgrading it to BufferType.EDITABLE if it was not already editable. Commas are excluded as they are added automatically by the view.

        // We don't care about watching text changes while appending.
        if (mTextWatcher != null) {
            removeTextChangedListener(mTextWatcher);
        }
        super.append(text, start, end);
        if (!TextUtils.isEmpty(text) && TextUtils.getTrimmedLength(text) > 0) {
            String displayString = text.toString();

            if (!displayString.trim().endsWith(String.valueOf(COMMIT_CHAR_COMMA))) {
                // We have no separator, so we should add it
                super.append(SEPARATOR, 0, SEPARATOR.length());
                displayString += SEPARATOR;
            }

            if (!TextUtils.isEmpty(displayString)
                    && TextUtils.getTrimmedLength(displayString) > 0) {
                mPendingChipsCount++;
                mPendingChips.add(displayString);
            }
        }
        // Put a message on the queue to make sure we ALWAYS handle pending
        // chips.
        if (mPendingChipsCount > 0) {
            postHandlePendingChips();
        }
        mHandler.post(mAddTextWatcher);
    
public voidappendRecipientEntry(RecipientEntry entry)
Append a new {@link RecipientEntry} to the end of the recipient chips, leaving any unfinished text at the end.

        clearComposingText();

        final Editable editable = getText();
        int chipInsertionPoint = 0;

        // Find the end of last chip and see if there's any unchipified text.
        final DrawableRecipientChip[] recips = getSortedRecipients();
        if (recips != null && recips.length > 0) {
            final DrawableRecipientChip last = recips[recips.length - 1];
            // The chip will be inserted at the end of last chip + 1. All the unfinished text after
            // the insertion point will be kept untouched.
            chipInsertionPoint = editable.getSpanEnd(last) + 1;
        }

        final CharSequence chip = createChip(entry, false);
        if (chip != null) {
            editable.insert(chipInsertionPoint, chip);
        }
    
private floatcalculateAvailableWidth()
Get the max amount of space a chip can take up. The formula takes into account the width of the EditTextView, any view padding, and padding that will be added to the chip.

        return getWidth() - getPaddingLeft() - getPaddingRight() - mChipTextStartPadding
                - mChipTextEndPadding;
    
private intcalculateOffsetFromBottom(int line)
Calculate the bottom of the line the chip will be located on using: 1) which line the chip appears on 2) the height of a chip 3) padding built into the edit text view

        // Line offsets start at zero.
        int actualLine = getLineCount() - (line + 1);
        return -((actualLine * ((int) mChipHeight) + getPaddingBottom()) + getPaddingTop())
                + getDropDownVerticalOffset();
    
private intcalculateOffsetFromBottomToTop(int line)
Calculate the offset from bottom of the EditText to top of the provided line.

        return -(int) ((mChipHeight + (2 * mLineSpacingExtra)) * (Math
                .abs(getLineCount() - line)) + getPaddingBottom());
    
private intcalculateTextHeight()

        final Rect textBounds = new Rect();
        final TextPaint paint = getPaint();

        textBounds.setEmpty();
        // First measure the bounds of a sample text.
        final String textHeightSample = "a";
        paint.getTextBounds(textHeightSample, 0, textHeightSample.length(), textBounds);

        textBounds.left = 0;
        textBounds.right = 0;

        return textBounds.height();
    
private voidcheckChipWidths()

        // Check the widths of the associated chips.
        DrawableRecipientChip[] chips = getSortedRecipients();
        if (chips != null) {
            Rect bounds;
            for (DrawableRecipientChip chip : chips) {
                bounds = chip.getBounds();
                if (getWidth() > 0 && bounds.right - bounds.left >
                        getWidth() - getPaddingLeft() - getPaddingRight()) {
                    // Need to redraw that chip.
                    replaceChip(chip, chip.getEntry());
                }
            }
        }
    
private booleanchipsPending()

        return mPendingChipsCount > 0 || (mRemovedSpans != null && mRemovedSpans.size() > 0);
    
private voidclearSelectedChip()

        if (mSelectedChip != null) {
            unselectChip(mSelectedChip);
            mSelectedChip = null;
        }
        setCursorVisible(true);
    
private voidcommitByCharacter()

        // We can't possibly commit by character if we can't tokenize.
        if (mTokenizer == null) {
            return;
        }
        Editable editable = getText();
        int end = getSelectionEnd();
        int start = mTokenizer.findTokenStart(editable, end);
        if (shouldCreateChip(start, end)) {
            commitChip(start, end, editable);
        }
        setSelection(getText().length());
    
private booleancommitChip(int start, int end, android.text.Editable editable)

        ListAdapter adapter = getAdapter();
        if (adapter != null && adapter.getCount() > 0 && enoughToFilter()
                && end == getSelectionEnd() && !isPhoneQuery()) {
            // let's choose the selected or first entry if only the input text is NOT an email
            // address so we won't try to replace the user's potentially correct but
            // new/unencountered email input
            if (!isValidEmailAddress(editable.toString().substring(start, end).trim())) {
                final int selectedPosition = getListSelection();
                if (selectedPosition == -1) {
                    // Nothing is selected; use the first item
                    submitItemAtPosition(0);
                } else {
                    submitItemAtPosition(selectedPosition);
                }
            }
            dismissDropDown();
            return true;
        } else {
            int tokenEnd = mTokenizer.findTokenEnd(editable, start);
            if (editable.length() > tokenEnd + 1) {
                char charAt = editable.charAt(tokenEnd + 1);
                if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) {
                    tokenEnd++;
                }
            }
            String text = editable.toString().substring(start, tokenEnd).trim();
            clearComposingText();
            if (text != null && text.length() > 0 && !text.equals(" ")) {
                RecipientEntry entry = createTokenizedEntry(text);
                if (entry != null) {
                    QwertyKeyListener.markAsReplaced(editable, start, end, "");
                    CharSequence chipText = createChip(entry, false);
                    if (chipText != null && start > -1 && end > -1) {
                        editable.replace(start, end, chipText);
                    }
                }
                // Only dismiss the dropdown if it is related to the text we
                // just committed.
                // For paste, it may not be as there are possibly multiple
                // tokens being added.
                if (end == getSelectionEnd()) {
                    dismissDropDown();
                }
                sanitizeBetween();
                return true;
            }
        }
        return false;
    
private booleancommitDefault()
Create a chip from the default selection. If the popup is showing, the default is the selected item (if one is selected), or the first item, in the popup suggestions list. Otherwise, it is whatever the user had typed in. End represents where the tokenizer should search for a token to turn into a chip.

return
If a chip was created from a real contact.

        // If there is no tokenizer, don't try to commit.
        if (mTokenizer == null) {
            return false;
        }
        Editable editable = getText();
        int end = getSelectionEnd();
        int start = mTokenizer.findTokenStart(editable, end);

        if (shouldCreateChip(start, end)) {
            int whatEnd = mTokenizer.findTokenEnd(getText(), start);
            // In the middle of chip; treat this as an edit
            // and commit the whole token.
            whatEnd = movePastTerminators(whatEnd);
            if (whatEnd != getSelectionEnd()) {
                handleEdit(start, whatEnd);
                return true;
            }
            return commitChip(start, end , editable);
        }
        return false;
    
private com.android.ex.chips.recipientchip.DrawableRecipientChipconstructChipSpan(RecipientEntry contact, boolean pressed)

        TextPaint paint = getPaint();
        float defaultSize = paint.getTextSize();
        int defaultColor = paint.getColor();

        Bitmap tmpBitmap;
        if (pressed) {
            tmpBitmap = createSelectedChip(contact, paint);

        } else {
            tmpBitmap = createUnselectedChip(contact, paint);
        }

        // Pass the full text, un-ellipsized, to the chip.
        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
        result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
        VisibleRecipientChip recipientChip =
                new VisibleRecipientChip(result, contact);
        recipientChip.setExtraMargin(mLineSpacingExtra);
        // Return text to the original size.
        paint.setTextSize(defaultSize);
        paint.setColor(defaultColor);
        return recipientChip;
    
private android.graphics.drawable.StateListDrawableconstructStateListDeleteDrawable()

        // Construct the StateListDrawable from deleteDrawable
        StateListDrawable deleteDrawable = new StateListDrawable();
        if (!mDisableDelete) {
            deleteDrawable.addState(new int[]{android.R.attr.state_activated}, mChipDelete);
        }
        deleteDrawable.addState(new int[0], null);
        return deleteDrawable;
    
intcountTokens(android.text.Editable text)

        int tokenCount = 0;
        int start = 0;
        while (start < text.length()) {
            start = movePastTerminators(mTokenizer.findTokenEnd(text, start));
            tokenCount++;
            if (start >= text.length()) {
                break;
            }
        }
        return tokenCount;
    
java.lang.StringcreateAddressText(RecipientEntry entry)

        String display = entry.getDisplayName();
        String address = entry.getDestination();
        if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
            display = null;
        }
        String trimmedDisplayText;
        if (isPhoneQuery() && isPhoneNumber(address)) {
            trimmedDisplayText = address.trim();
        } else {
            if (address != null) {
                // Tokenize out the address in case the address already
                // contained the username as well.
                Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(address);
                if (tokenized != null && tokenized.length > 0) {
                    address = tokenized[0].getAddress();
                }
            }
            Rfc822Token token = new Rfc822Token(display, address, null);
            trimmedDisplayText = token.toString().trim();
        }
        int index = trimmedDisplayText.indexOf(",");
        return mTokenizer != null && !TextUtils.isEmpty(trimmedDisplayText)
                && index < trimmedDisplayText.length() - 1 ? (String) mTokenizer
                .terminateToken(trimmedDisplayText) : trimmedDisplayText;
    
private android.widget.ListAdaptercreateAlternatesAdapter(com.android.ex.chips.recipientchip.DrawableRecipientChip chip)

        return new RecipientAlternatesAdapter(getContext(), chip.getContactId(),
                chip.getDirectoryId(), chip.getLookupKey(), chip.getDataId(),
                getAdapter().getQueryType(), this, mDropdownChipLayouter,
                constructStateListDeleteDrawable());
    
private java.lang.CharSequencecreateChip(RecipientEntry entry, boolean pressed)

        final String displayText = createAddressText(entry);
        if (TextUtils.isEmpty(displayText)) {
            return null;
        }
        // Always leave a blank space at the end of a chip.
        final int textLength = displayText.length() - 1;
        final SpannableString  chipText = new SpannableString(displayText);
        if (!mNoChips) {
            try {
                DrawableRecipientChip chip = constructChipSpan(entry, pressed);
                chipText.setSpan(chip, 0, textLength,
                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                chip.setOriginalText(chipText.toString());
            } catch (NullPointerException e) {
                Log.e(TAG, e.getMessage(), e);
                return null;
            }
        }
        onChipCreated(entry);
        return chipText;
    
private com.android.ex.chips.RecipientEditTextView$ChipBitmapContainercreateChipBitmap(RecipientEntry contact, android.text.TextPaint paint, android.graphics.drawable.Drawable overrideBackgroundDrawable, int backgroundColor)

        final ChipBitmapContainer result = new ChipBitmapContainer();

        Rect backgroundPadding = new Rect();
        if (overrideBackgroundDrawable != null) {
            overrideBackgroundDrawable.getPadding(backgroundPadding);
        }

        // Ellipsize the text so that it takes AT MOST the entire width of the
        // autocomplete text entry area. Make sure to leave space for padding
        // on the sides.
        int height = (int) mChipHeight;
        // Since the icon is a square, it's width is equal to the maximum height it can be inside
        // the chip. Don't include iconWidth for invalid contacts.
        int iconWidth = contact.isValid() ?
                height - backgroundPadding.top - backgroundPadding.bottom : 0;
        float[] widths = new float[1];
        paint.getTextWidths(" ", widths);
        CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
                calculateAvailableWidth() - iconWidth - widths[0] - backgroundPadding.left
                - backgroundPadding.right);
        int textWidth = (int) paint.measureText(ellipsizedText, 0, ellipsizedText.length());

        // Chip start padding is the same as the end padding if there is no contact image.
        final int startPadding = contact.isValid() ? mChipTextStartPadding : mChipTextEndPadding;
        // Make sure there is a minimum chip width so the user can ALWAYS
        // tap a chip without difficulty.
        int width = Math.max(iconWidth * 2, textWidth + startPadding + mChipTextEndPadding
                + iconWidth + backgroundPadding.left + backgroundPadding.right);

        // Create the background of the chip.
        result.bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(result.bitmap);

        // Check if the background drawable is set via attr
        if (overrideBackgroundDrawable != null) {
            overrideBackgroundDrawable.setBounds(0, 0, width, height);
            overrideBackgroundDrawable.draw(canvas);
        } else {
            // Draw the default chip background
            mWorkPaint.reset();
            mWorkPaint.setColor(backgroundColor);
            final float radius = height / 2;
            canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius,
                    mWorkPaint);
        }

        // Draw the text vertically aligned
        int textX = shouldPositionAvatarOnRight() ?
                mChipTextEndPadding + backgroundPadding.left :
                width - backgroundPadding.right - mChipTextEndPadding - textWidth;
        canvas.drawText(ellipsizedText, 0, ellipsizedText.length(),
                textX, getTextYOffset(height), paint);

        // Set the variables that are needed to draw the icon bitmap once it's loaded
        int iconX = shouldPositionAvatarOnRight() ? width - backgroundPadding.right - iconWidth :
                backgroundPadding.left;
        result.left = iconX;
        result.top = backgroundPadding.top;
        result.right = iconX + iconWidth;
        result.bottom = height - backgroundPadding.bottom;

        return result;
    
java.lang.StringcreateChipDisplayText(RecipientEntry entry)

        String display = entry.getDisplayName();
        String address = entry.getDestination();
        if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
            display = null;
        }
        if (!TextUtils.isEmpty(display)) {
            return display;
        } else if (!TextUtils.isEmpty(address)){
            return address;
        } else {
            return new Rfc822Token(display, address, null).toString();
        }
    
voidcreateMoreChip()
Create the more chip. The more chip is text that replaces any chips that do not fit in the pre-defined available space when the RecipientEditTextView loses focus.

        if (mNoChips) {
            createMoreChipPlainText();
            return;
        }

        if (!mShouldShrink) {
            return;
        }
        ReplacementDrawableSpan[] tempMore = getSpannable().getSpans(0, getText().length(),
                MoreImageSpan.class);
        if (tempMore.length > 0) {
            getSpannable().removeSpan(tempMore[0]);
        }
        DrawableRecipientChip[] recipients = getSortedRecipients();

        if (recipients == null || recipients.length <= CHIP_LIMIT) {
            mMoreChip = null;
            return;
        }
        Spannable spannable = getSpannable();
        int numRecipients = recipients.length;
        int overage = numRecipients - CHIP_LIMIT;
        MoreImageSpan moreSpan = createMoreSpan(overage);
        mRemovedSpans = new ArrayList<DrawableRecipientChip>();
        int totalReplaceStart = 0;
        int totalReplaceEnd = 0;
        Editable text = getText();
        for (int i = numRecipients - overage; i < recipients.length; i++) {
            mRemovedSpans.add(recipients[i]);
            if (i == numRecipients - overage) {
                totalReplaceStart = spannable.getSpanStart(recipients[i]);
            }
            if (i == recipients.length - 1) {
                totalReplaceEnd = spannable.getSpanEnd(recipients[i]);
            }
            if (mTemporaryRecipients == null || !mTemporaryRecipients.contains(recipients[i])) {
                int spanStart = spannable.getSpanStart(recipients[i]);
                int spanEnd = spannable.getSpanEnd(recipients[i]);
                recipients[i].setOriginalText(text.toString().substring(spanStart, spanEnd));
            }
            spannable.removeSpan(recipients[i]);
        }
        if (totalReplaceEnd < text.length()) {
            totalReplaceEnd = text.length();
        }
        int end = Math.max(totalReplaceStart, totalReplaceEnd);
        int start = Math.min(totalReplaceStart, totalReplaceEnd);
        SpannableString chipText = new SpannableString(text.subSequence(start, end));
        chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        text.replace(start, end, chipText);
        mMoreChip = moreSpan;
        // If adding the +more chip goes over the limit, resize accordingly.
        if (!isPhoneQuery() && getLineCount() > mMaxLines) {
            setMaxLines(getLineCount());
        }
    
voidcreateMoreChipPlainText()

        // Take the first <= CHIP_LIMIT addresses and get to the end of the second one.
        Editable text = getText();
        int start = 0;
        int end = start;
        for (int i = 0; i < CHIP_LIMIT; i++) {
            end = movePastTerminators(mTokenizer.findTokenEnd(text, start));
            start = end; // move to the next token and get its end.
        }
        // Now, count total addresses.
        start = 0;
        int tokenCount = countTokens(text);
        MoreImageSpan moreSpan = createMoreSpan(tokenCount - CHIP_LIMIT);
        SpannableString chipText = new SpannableString(text.subSequence(end, text.length()));
        chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        text.replace(end, text.length(), chipText);
        mMoreChip = moreSpan;
    
private com.android.ex.chips.RecipientEditTextView$MoreImageSpancreateMoreSpan(int count)

        String moreText = String.format(mMoreItem.getText().toString(), count);
        mWorkPaint.set(getPaint());
        mWorkPaint.setTextSize(mMoreItem.getTextSize());
        mWorkPaint.setColor(mMoreItem.getCurrentTextColor());
        final int width = (int) mWorkPaint.measureText(moreText) + mMoreItem.getPaddingLeft()
                + mMoreItem.getPaddingRight();
        final int height = (int) mChipHeight;
        Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(drawable);
        int adjustedHeight = height;
        Layout layout = getLayout();
        if (layout != null) {
            adjustedHeight -= layout.getLineDescent(0);
        }
        canvas.drawText(moreText, 0, moreText.length(), 0, adjustedHeight, mWorkPaint);

        Drawable result = new BitmapDrawable(getResources(), drawable);
        result.setBounds(0, 0, width, height);
        return new MoreImageSpan(result);
    
voidcreateReplacementChip(int tokenStart, int tokenEnd, android.text.Editable editable, boolean visible)
Create a chip that represents just the email address of a recipient. At some later point, this chip will be attached to a real contact entry, if one exists.

        if (alreadyHasChip(tokenStart, tokenEnd)) {
            // There is already a chip present at this location.
            // Don't recreate it.
            return;
        }
        String token = editable.toString().substring(tokenStart, tokenEnd);
        final String trimmedToken = token.trim();
        int commitCharIndex = trimmedToken.lastIndexOf(COMMIT_CHAR_COMMA);
        if (commitCharIndex != -1 && commitCharIndex == trimmedToken.length() - 1) {
            token = trimmedToken.substring(0, trimmedToken.length() - 1);
        }
        RecipientEntry entry = createTokenizedEntry(token);
        if (entry != null) {
            DrawableRecipientChip chip = null;
            try {
                if (!mNoChips) {
                    chip = visible ?
                            constructChipSpan(entry, false) : new InvisibleRecipientChip(entry);
                }
            } catch (NullPointerException e) {
                Log.e(TAG, e.getMessage(), e);
            }
            editable.setSpan(chip, tokenStart, tokenEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            // Add this chip to the list of entries "to replace"
            if (chip != null) {
                if (mTemporaryRecipients == null) {
                    mTemporaryRecipients = new ArrayList<DrawableRecipientChip>();
                }
                chip.setOriginalText(token);
                mTemporaryRecipients.add(chip);
            }
        }
    
private android.graphics.BitmapcreateSelectedChip(RecipientEntry contact, android.text.TextPaint paint)
Creates a bitmap of the given contact on a selected chip.

param
contact The recipient entry to pull data from.
param
paint The paint to use to draw the bitmap.

        paint.setColor(sSelectedTextColor);
        final ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint,
                mChipBackgroundPressed, getResources().getColor(R.color.chip_background_selected));

        if (bitmapContainer.loadIcon) {
            loadAvatarIcon(contact, bitmapContainer);
        }

        return bitmapContainer.bitmap;
    
private android.widget.ListAdaptercreateSingleAddressAdapter(com.android.ex.chips.recipientchip.DrawableRecipientChip currentChip)

        return new SingleRecipientArrayAdapter(getContext(), currentChip.getEntry(),
                mDropdownChipLayouter, constructStateListDeleteDrawable());
    
RecipientEntrycreateTokenizedEntry(java.lang.String token)

        if (TextUtils.isEmpty(token)) {
            return null;
        }
        if (isPhoneQuery() && isPhoneNumber(token)) {
            return RecipientEntry.constructFakePhoneEntry(token, true);
        }
        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token);
        String display = null;
        boolean isValid = isValid(token);
        if (isValid && tokens != null && tokens.length > 0) {
            // If we can get a name from tokenizing, then generate an entry from
            // this.
            display = tokens[0].getName();
            if (!TextUtils.isEmpty(display)) {
                return RecipientEntry.constructGeneratedEntry(display, tokens[0].getAddress(),
                        isValid);
            } else {
                display = tokens[0].getAddress();
                if (!TextUtils.isEmpty(display)) {
                    return RecipientEntry.constructFakeEntry(display, isValid);
                }
            }
        }
        // Unable to validate the token or to create a valid token from it.
        // Just create a chip the user can edit.
        String validatedToken = null;
        if (mValidator != null && !isValid) {
            // Try fixing up the entry using the validator.
            validatedToken = mValidator.fixText(token).toString();
            if (!TextUtils.isEmpty(validatedToken)) {
                if (validatedToken.contains(token)) {
                    // protect against the case of a validator with a null
                    // domain,
                    // which doesn't add a domain to the token
                    Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(validatedToken);
                    if (tokenized.length > 0) {
                        validatedToken = tokenized[0].getAddress();
                        isValid = true;
                    }
                } else {
                    // We ran into a case where the token was invalid and
                    // removed
                    // by the validator. In this case, just use the original
                    // token
                    // and let the user sort out the error chip.
                    validatedToken = null;
                    isValid = false;
                }
            }
        }
        // Otherwise, fallback to just creating an editable email address chip.
        return RecipientEntry.constructFakeEntry(
                !TextUtils.isEmpty(validatedToken) ? validatedToken : token, isValid);
    
private android.graphics.BitmapcreateUnselectedChip(RecipientEntry contact, android.text.TextPaint paint)
Creates a bitmap of the given contact on a selected chip.

param
contact The recipient entry to pull data from.
param
paint The paint to use to draw the bitmap.

        paint.setColor(getContext().getResources().getColor(android.R.color.black));
        ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint,
                getChipBackground(contact), getDefaultChipBackgroundColor(contact));

        if (bitmapContainer.loadIcon) {
            loadAvatarIcon(contact, bitmapContainer);
        }
        return bitmapContainer.bitmap;
    
private RecipientEntrycreateValidatedEntry(RecipientEntry item)

        if (item == null) {
            return null;
        }
        final RecipientEntry entry;
        // If the display name and the address are the same, or if this is a
        // valid contact, but the destination is invalid, then make this a fake
        // recipient that is editable.
        String destination = item.getDestination();
        if (!isPhoneQuery() && item.getContactId() == RecipientEntry.GENERATED_CONTACT) {
            entry = RecipientEntry.constructGeneratedEntry(item.getDisplayName(),
                    destination, item.isValid());
        } else if (RecipientEntry.isCreatedRecipient(item.getContactId())
                && (TextUtils.isEmpty(item.getDisplayName())
                        || TextUtils.equals(item.getDisplayName(), destination)
                        || (mValidator != null && !mValidator.isValid(destination)))) {
            entry = RecipientEntry.constructFakeEntry(destination, item.isValid());
        } else {
            entry = item;
        }
        return entry;
    
private voiddrawIcon(com.android.ex.chips.RecipientEditTextView$ChipBitmapContainer bitMapResult, android.graphics.Bitmap icon)
Helper function that draws the loaded icon bitmap into the chips bitmap

        final Canvas canvas = new Canvas(bitMapResult.bitmap);
        final RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight());
        final RectF dst = new RectF(bitMapResult.left, bitMapResult.top, bitMapResult.right,
                bitMapResult.bottom);
        drawIconOnCanvas(icon, canvas, src, dst);
    
protected voiddrawIconOnCanvas(android.graphics.Bitmap icon, android.graphics.Canvas canvas, android.graphics.RectF src, android.graphics.RectF dst)
Draws the icon onto the canvas given the source rectangle of the bitmap and the destination rectangle of the canvas.

        final Matrix matrix = new Matrix();

        // Draw bitmap through shader first.
        final BitmapShader shader = new BitmapShader(icon, TileMode.CLAMP, TileMode.CLAMP);
        matrix.reset();

        // Fit bitmap to bounds.
        matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);

        shader.setLocalMatrix(matrix);
        mWorkPaint.reset();
        mWorkPaint.setShader(shader);
        mWorkPaint.setAntiAlias(true);
        mWorkPaint.setFilterBitmap(true);
        mWorkPaint.setDither(true);
        canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f, mWorkPaint);

        // Then draw the border.
        final float borderWidth = 1f;
        mWorkPaint.reset();
        mWorkPaint.setColor(Color.TRANSPARENT);
        mWorkPaint.setStyle(Style.STROKE);
        mWorkPaint.setStrokeWidth(borderWidth);
        mWorkPaint.setAntiAlias(true);
        canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f - borderWidth / 2, mWorkPaint);

        mWorkPaint.reset();
    
private java.lang.CharSequenceellipsizeText(java.lang.CharSequence text, android.text.TextPaint paint, float maxWidth)

        paint.setTextSize(mChipFontSize);
        if (maxWidth <= 0 && Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Max width is negative: " + maxWidth);
        }
        return TextUtils.ellipsize(text, paint, maxWidth,
                TextUtils.TruncateAt.END);
    
public voidenableDrag()
Enables drag-and-drop for chips.

        mDragEnabled = true;
    
private voidexpand()

        if (mShouldShrink) {
            setMaxLines(Integer.MAX_VALUE);
        }
        removeMoreChip();
        setCursorVisible(true);
        Editable text = getText();
        setSelection(text != null && text.length() > 0 ? text.length() : 0);
        // If there are any temporary chips, try replacing them now that the user
        // has expanded the field.
        if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0) {
            new RecipientReplacementTask().execute();
            mTemporaryRecipients = null;
        }
    
private com.android.ex.chips.recipientchip.DrawableRecipientChipfindChip(int offset)

        DrawableRecipientChip[] chips =
                getSpannable().getSpans(0, getText().length(), DrawableRecipientChip.class);
        // Find the chip that contains this offset.
        for (int i = 0; i < chips.length; i++) {
            DrawableRecipientChip chip = chips[i];
            int start = getChipStart(chip);
            int end = getChipEnd(chip);
            if (offset >= start && offset <= end) {
                return chip;
            }
        }
        return null;
    
private static intfindText(android.text.Editable text, int offset)

        if (text.charAt(offset) != ' ") {
            return offset;
        }
        return -1;
    
private booleanfocusNext()

        View next = focusSearch(View.FOCUS_DOWN);
        if (next != null) {
            next.requestFocus();
            return true;
        }
        return false;
    
public BaseRecipientAdaptergetAdapter()

        return (BaseRecipientAdapter) super.getAdapter();
    
android.graphics.drawable.DrawablegetChipBackground(RecipientEntry contact)
Get the background drawable for a RecipientChip.

        return contact.isValid() ? mChipBackground : mInvalidChipBackground;
    
private intgetChipEnd(com.android.ex.chips.recipientchip.DrawableRecipientChip chip)

        return getSpannable().getSpanEnd(chip);
    
public floatgetChipHeight()

        return mChipHeight;
    
private intgetChipStart(com.android.ex.chips.recipientchip.DrawableRecipientChip chip)

        return getSpannable().getSpanStart(chip);
    
private intgetDefaultChipBackgroundColor(RecipientEntry contact)

        return getResources().getColor(contact.isValid() ? R.color.chip_background :
                R.color.chip_background_invalid);
    
private intgetExcessTopPadding()

        if (sExcessTopPadding == -1) {
            sExcessTopPadding = (int) (mChipHeight + mLineSpacingExtra);
        }
        return sExcessTopPadding;
    
com.android.ex.chips.recipientchip.DrawableRecipientChipgetLastChip()

        DrawableRecipientChip last = null;
        DrawableRecipientChip[] chips = getSortedRecipients();
        if (chips != null && chips.length > 0) {
            last = chips[chips.length - 1];
        }
        return last;
    
com.android.ex.chips.recipientchip.ReplacementDrawableSpangetMoreChip()

        MoreImageSpan[] moreSpans = getSpannable().getSpans(0, getText().length(),
                MoreImageSpan.class);
        return moreSpans != null && moreSpans.length > 0 ? moreSpans[0] : null;
    
protected android.widget.ScrollViewgetScrollView()

        return mScrollView;
    
public java.util.ListgetSelectedRecipients()

return
The list of {@link RecipientEntry}s that have been selected by the user.

        DrawableRecipientChip[] chips =
                getText().getSpans(0, getText().length(), DrawableRecipientChip.class);
        List<RecipientEntry> results = new ArrayList();
        if (chips == null) {
            return results;
        }

        for (DrawableRecipientChip c : chips) {
            results.add(c.getEntry());
        }

        return results;
    
com.android.ex.chips.recipientchip.DrawableRecipientChip[]getSortedRecipients()

        DrawableRecipientChip[] recips = getSpannable()
                .getSpans(0, getText().length(), DrawableRecipientChip.class);
        ArrayList<DrawableRecipientChip> recipientsList = new ArrayList<DrawableRecipientChip>(
                Arrays.asList(recips));
        final Spannable spannable = getSpannable();
        Collections.sort(recipientsList, new Comparator<DrawableRecipientChip>() {

            @Override
            public int compare(DrawableRecipientChip first, DrawableRecipientChip second) {
                int firstStart = spannable.getSpanStart(first);
                int secondStart = spannable.getSpanStart(second);
                if (firstStart < secondStart) {
                    return -1;
                } else if (firstStart > secondStart) {
                    return 1;
                } else {
                    return 0;
                }
            }
        });
        return recipientsList.toArray(new DrawableRecipientChip[recipientsList.size()]);
    
android.text.SpannablegetSpannable()

        return getText();
    
protected floatgetTextYOffset(int height)
Given a height, returns a Y offset that will draw the text in the middle of the height.

        return height - ((height - mTextHeight) / 2);
    
intgetViewWidth()

        return getWidth();
    
private voidhandleEdit(int start, int end)

        if (start == -1 || end == -1) {
            // This chip no longer exists in the field.
            dismissDropDown();
            return;
        }
        // This is in the middle of a chip, so select out the whole chip
        // and commit it.
        Editable editable = getText();
        setSelection(end);
        String text = getText().toString().substring(start, end);
        if (!TextUtils.isEmpty(text)) {
            RecipientEntry entry = RecipientEntry.constructFakeEntry(text, isValid(text));
            QwertyKeyListener.markAsReplaced(editable, start, end, "");
            CharSequence chipText = createChip(entry, false);
            int selEnd = getSelectionEnd();
            if (chipText != null && start > -1 && selEnd > -1) {
                editable.replace(start, selEnd, chipText);
            }
        }
        dismissDropDown();
    
java.util.ArrayListhandlePaste()

        String text = getText().toString();
        int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
        String lastAddress = text.substring(originalTokenStart);
        int tokenStart = originalTokenStart;
        int prevTokenStart = 0;
        DrawableRecipientChip findChip = null;
        ArrayList<DrawableRecipientChip> created = new ArrayList<DrawableRecipientChip>();
        if (tokenStart != 0) {
            // There are things before this!
            while (tokenStart != 0 && findChip == null && tokenStart != prevTokenStart) {
                prevTokenStart = tokenStart;
                tokenStart = mTokenizer.findTokenStart(text, tokenStart);
                findChip = findChip(tokenStart);
                if (tokenStart == originalTokenStart && findChip == null) {
                    break;
                }
            }
            if (tokenStart != originalTokenStart) {
                if (findChip != null) {
                    tokenStart = prevTokenStart;
                }
                int tokenEnd;
                DrawableRecipientChip createdChip;
                while (tokenStart < originalTokenStart) {
                    tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(getText().toString(),
                            tokenStart));
                    commitChip(tokenStart, tokenEnd, getText());
                    createdChip = findChip(tokenStart);
                    if (createdChip == null) {
                        break;
                    }
                    // +1 for the space at the end.
                    tokenStart = getSpannable().getSpanEnd(createdChip) + 1;
                    created.add(createdChip);
                }
            }
        }
        // Take a look at the last token. If the token has been completed with a
        // commit character, create a chip.
        if (isCompletedToken(lastAddress)) {
            Editable editable = getText();
            tokenStart = editable.toString().indexOf(lastAddress, originalTokenStart);
            commitChip(tokenStart, editable.length(), editable);
            created.add(findChip(tokenStart));
        }
        return created;
    
private voidhandlePasteAndReplace()

        ArrayList<DrawableRecipientChip> created = handlePaste();
        if (created != null && created.size() > 0) {
            // Perform reverse lookups on the pasted contacts.
            IndividualReplacementTask replace = new IndividualReplacementTask();
            replace.execute(created);
        }
    
voidhandlePasteClip(android.content.ClipData clip)
Handles pasting a {@link ClipData} to this {@link RecipientEditTextView}.

        if (clip == null) {
            // Do nothing.
            return;
        }

        final ClipDescription clipDesc = clip.getDescription();
        boolean containsSupportedType = clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
                clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
        if (!containsSupportedType) {
            return;
        }

        removeTextChangedListener(mTextWatcher);

        final ClipDescription clipDescription = clip.getDescription();
        for (int i = 0; i < clip.getItemCount(); i++) {
            final String mimeType = clipDescription.getMimeType(i);
            final boolean supportedType = ClipDescription.MIMETYPE_TEXT_PLAIN.equals(mimeType) ||
                    ClipDescription.MIMETYPE_TEXT_HTML.equals(mimeType);
            if (!supportedType) {
                // Only plain text and html can be pasted.
                continue;
            }

            final CharSequence pastedItem = clip.getItemAt(i).getText();
            if (!TextUtils.isEmpty(pastedItem)) {
                final Editable editable = getText();
                final int start = getSelectionStart();
                final int end = getSelectionEnd();
                if (start < 0 || end < 1) {
                    // No selection.
                    editable.append(pastedItem);
                } else if (start == end) {
                    // Insert at position.
                    editable.insert(start, pastedItem);
                } else {
                    editable.append(pastedItem, start, end);
                }
                handlePasteAndReplace();
            }
        }

        mHandler.post(mAddTextWatcher);
    
voidhandlePendingChips()

        if (getViewWidth() <= 0) {
            // The widget has not been sized yet.
            // This will be called as a result of onSizeChanged
            // at a later point.
            return;
        }
        if (mPendingChipsCount <= 0) {
            return;
        }

        synchronized (mPendingChips) {
            Editable editable = getText();
            // Tokenize!
            if (mPendingChipsCount <= MAX_CHIPS_PARSED) {
                for (int i = 0; i < mPendingChips.size(); i++) {
                    String current = mPendingChips.get(i);
                    int tokenStart = editable.toString().indexOf(current);
                    // Always leave a space at the end between tokens.
                    int tokenEnd = tokenStart + current.length() - 1;
                    if (tokenStart >= 0) {
                        // When we have a valid token, include it with the token
                        // to the left.
                        if (tokenEnd < editable.length() - 2
                                && editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) {
                            tokenEnd++;
                        }
                        createReplacementChip(tokenStart, tokenEnd, editable, i < CHIP_LIMIT
                                || !mShouldShrink);
                    }
                    mPendingChipsCount--;
                }
                sanitizeEnd();
            } else {
                mNoChips = true;
            }

            if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0
                    && mTemporaryRecipients.size() <= RecipientAlternatesAdapter.MAX_LOOKUPS) {
                if (hasFocus() || mTemporaryRecipients.size() < CHIP_LIMIT) {
                    new RecipientReplacementTask().execute();
                    mTemporaryRecipients = null;
                } else {
                    // Create the "more" chip
                    mIndividualReplacements = new IndividualReplacementTask();
                    mIndividualReplacements.execute(new ArrayList<DrawableRecipientChip>(
                            mTemporaryRecipients.subList(0, CHIP_LIMIT)));
                    if (mTemporaryRecipients.size() > CHIP_LIMIT) {
                        mTemporaryRecipients = new ArrayList<DrawableRecipientChip>(
                                mTemporaryRecipients.subList(CHIP_LIMIT,
                                        mTemporaryRecipients.size()));
                    } else {
                        mTemporaryRecipients = null;
                    }
                    createMoreChip();
                }
            } else {
                // There are too many recipients to look up, so just fall back
                // to showing addresses for all of them.
                mTemporaryRecipients = null;
                createMoreChip();
            }
            mPendingChipsCount = 0;
            mPendingChips.clear();
        }
    
booleanisCompletedToken(java.lang.CharSequence text)

        if (TextUtils.isEmpty(text)) {
            return false;
        }
        // Check to see if this is a completed token before filtering.
        int end = text.length();
        int start = mTokenizer.findTokenStart(text, end);
        String token = text.toString().substring(start, end).trim();
        if (!TextUtils.isEmpty(token)) {
            char atEnd = token.charAt(token.length() - 1);
            return atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON;
        }
        return false;
    
public booleanisGeneratedContact(com.android.ex.chips.recipientchip.DrawableRecipientChip chip)

        long contactId = chip.getContactId();
        return contactId == RecipientEntry.INVALID_CONTACT
                || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
    
private static booleanisPhoneNumber(java.lang.String number)

        // TODO: replace this function with libphonenumber's isPossibleNumber (see
        // PhoneNumberUtil). One complication is that it requires the sender's region which
        // comes from the CurrentCountryIso. For now, let's just do this simple match.
        if (TextUtils.isEmpty(number)) {
            return false;
        }

        Matcher match = PHONE_PATTERN.matcher(number);
        return match.matches();
    
protected booleanisPhoneQuery()

        return getAdapter() != null
                && getAdapter().getQueryType() == BaseRecipientAdapter.QUERY_TYPE_PHONE;
    
private booleanisValid(java.lang.String text)

        return mValidator == null ? true : mValidator.isValid(text);
    
private booleanisValidEmailAddress(java.lang.String input)

        return !TextUtils.isEmpty(input) && mValidator != null &&
                mValidator.isValid(input);
    
public booleanlastCharacterIsCommitCharacter(java.lang.CharSequence s)

        char last;
        int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
        int len = length() - 1;
        if (end != len) {
            last = s.charAt(end);
        } else {
            last = s.charAt(len);
        }
        return last == COMMIT_CHAR_COMMA || last == COMMIT_CHAR_SEMICOLON;
    
private voidloadAvatarIcon(RecipientEntry contact, com.android.ex.chips.RecipientEditTextView$ChipBitmapContainer bitmapContainer)
Returns the avatar icon to use for this recipient entry. Returns null if we don't want to draw an icon for this recipient.

        // Don't draw photos for recipients that have been typed in OR generated on the fly.
        long contactId = contact.getContactId();
        boolean drawPhotos = isPhoneQuery() ?
                contactId != RecipientEntry.INVALID_CONTACT
                : (contactId != RecipientEntry.INVALID_CONTACT
                        && contactId != RecipientEntry.GENERATED_CONTACT);

        if (drawPhotos) {
            final byte[] origPhotoBytes = contact.getPhotoBytes();
            // There may not be a photo yet if anything but the first contact address
            // was selected.
            if (origPhotoBytes == null) {
                // TODO: cache this in the recipient entry?
                getAdapter().fetchPhoto(contact, new PhotoManager.PhotoManagerCallback() {
                    @Override
                    public void onPhotoBytesPopulated() {
                        // Call through to the async version which will ensure
                        // proper threading.
                        onPhotoBytesAsynchronouslyPopulated();
                    }

                    @Override
                    public void onPhotoBytesAsynchronouslyPopulated() {
                        final byte[] loadedPhotoBytes = contact.getPhotoBytes();
                        final Bitmap icon = BitmapFactory.decodeByteArray(loadedPhotoBytes, 0,
                                loadedPhotoBytes.length);
                        tryDrawAndInvalidate(icon);
                    }

                    @Override
                    public void onPhotoBytesAsyncLoadFailed() {
                        // TODO: can the scaled down default photo be cached?
                        tryDrawAndInvalidate(mDefaultContactPhoto);
                    }

                    private void tryDrawAndInvalidate(Bitmap icon) {
                        drawIcon(bitmapContainer, icon);
                        // The caller might originated from a background task. However, if the
                        // background task has already completed, the view might be already drawn
                        // on the UI but the callback would happen on the background thread.
                        // So if we are on a background thread, post an invalidate call to the UI.
                        if (Looper.myLooper() == Looper.getMainLooper()) {
                            // The view might not redraw itself since it's loaded asynchronously
                            invalidate();
                        } else {
                            post(new Runnable() {
                                @Override
                                public void run() {
                                    invalidate();
                                }
                            });
                        }
                    }
                });
            } else {
                final Bitmap icon = BitmapFactory.decodeByteArray(origPhotoBytes, 0,
                        origPhotoBytes.length);
                drawIcon(bitmapContainer, icon);
            }
        }
    
intmovePastTerminators(int tokenEnd)

        if (tokenEnd >= length()) {
            return tokenEnd;
        }
        char atEnd = getText().toString().charAt(tokenEnd);
        if (atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON) {
            tokenEnd++;
        }
        // This token had not only an end token character, but also a space
        // separating it from the next token.
        if (tokenEnd < length() && getText().toString().charAt(tokenEnd) == ' ") {
            tokenEnd++;
        }
        return tokenEnd;
    
public booleanonActionItemClicked(android.view.ActionMode mode, android.view.MenuItem item)

        return false;
    
protected voidonAttachedToWindow()

        super.onAttachedToWindow();
        mAttachedToWindow = true;
    
public voidonCheckedItemChanged(int position)

        ListView listView = mAlternatesPopup.getListView();
        if (listView != null && listView.getCheckedItemCount() == 0) {
            listView.setItemChecked(position, true);
        }
        mCheckedItem = position;
    
protected voidonChipCreated(RecipientEntry entry)
A callback for subclasses to use to know when a chip was created with the given RecipientEntry.

public voidonChipDelete()

        if (mSelectedChip != null) {
            removeChip(mSelectedChip);
        }
        mAddressPopup.dismiss();
        mAlternatesPopup.dismiss();
    
public voidonClick(com.android.ex.chips.recipientchip.DrawableRecipientChip chip)
Handle click events for a chip. When a selected chip receives a click event, see if that event was in the delete icon. If so, delete it. Otherwise, unselect the chip.

        if (chip.isSelected()) {
            clearSelectedChip();
        }
    
public voidonClick(android.view.View v)

        // Copy this to the clipboard.
        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
                Context.CLIPBOARD_SERVICE);
        clipboard.setPrimaryClip(ClipData.newPlainText("", mCopyAddress));
        mCopyDialog.dismiss();
    
public booleanonCreateActionMode(android.view.ActionMode mode, android.view.Menu menu)
No chips are selectable.

        return false;
    
public android.view.inputmethod.InputConnectiononCreateInputConnection(android.view.inputmethod.EditorInfo outAttrs)

        InputConnection connection = super.onCreateInputConnection(outAttrs);
        int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION;
        if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) {
            // clear the existing action
            outAttrs.imeOptions ^= imeActions;
            // set the DONE action
            outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
        }
        if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
            outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
        }

        outAttrs.actionId = EditorInfo.IME_ACTION_DONE;
        outAttrs.actionLabel = getContext().getString(R.string.action_label);
        return connection;
    
public voidonDestroyActionMode(android.view.ActionMode mode)

    
protected voidonDetachedFromWindow()

        super.onDetachedFromWindow();
        mAttachedToWindow = false;
    
public voidonDismiss(android.content.DialogInterface dialog)

        mCopyAddress = null;
    
public booleanonDown(android.view.MotionEvent e)

        return false;
    
public booleanonDragEvent(android.view.DragEvent event)
Handles drag event.

        switch (event.getAction()) {
            case DragEvent.ACTION_DRAG_STARTED:
                // Only handle plain text drag and drop.
                return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
            case DragEvent.ACTION_DRAG_ENTERED:
                requestFocus();
                return true;
            case DragEvent.ACTION_DROP:
                handlePasteClip(event.getClipData());
                return true;
        }
        return false;
    
public booleanonEditorAction(android.widget.TextView view, int action, android.view.KeyEvent keyEvent)

        if (action == EditorInfo.IME_ACTION_DONE) {
            if (commitDefault()) {
                return true;
            }
            if (mSelectedChip != null) {
                clearSelectedChip();
                return true;
            } else if (focusNext()) {
                return true;
            }
        }
        return false;
    
public booleanonFling(android.view.MotionEvent e1, android.view.MotionEvent e2, float velocityX, float velocityY)

        // Do nothing.
        return false;
    
public voidonFocusChanged(boolean hasFocus, int direction, android.graphics.Rect previous)

        super.onFocusChanged(hasFocus, direction, previous);
        if (!hasFocus) {
            shrink();
        } else {
            expand();
        }
    
public voidonItemClick(android.widget.AdapterView parent, android.view.View view, int position, long id)
When an item in the suggestions list has been clicked, create a chip from the contact information of the selected item.

        if (position < 0) {
            return;
        }

        final int charactersTyped = submitItemAtPosition(position);
        if (charactersTyped > -1 && mRecipientEntryItemClickedListener != null) {
            mRecipientEntryItemClickedListener
                    .onRecipientEntryItemClicked(charactersTyped, position);
        }
    
public booleanonKeyDown(int keyCode, android.view.KeyEvent event)
If there is a selected chip, delegate the key events to the selected chip.

        if (mSelectedChip != null && keyCode == KeyEvent.KEYCODE_DEL) {
            if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
                mAlternatesPopup.dismiss();
            }
            removeChip(mSelectedChip);
        }

        switch (keyCode) {
            case KeyEvent.KEYCODE_ENTER:
            case KeyEvent.KEYCODE_DPAD_CENTER:
                if (event.hasNoModifiers()) {
                    if (commitDefault()) {
                        return true;
                    }
                    if (mSelectedChip != null) {
                        clearSelectedChip();
                        return true;
                    } else if (focusNext()) {
                        return true;
                    }
                }
                break;
        }

        return super.onKeyDown(keyCode, event);
    
public booleanonKeyPreIme(int keyCode, android.view.KeyEvent event)
Dismiss any selected chips when the back key is pressed.

        if (keyCode == KeyEvent.KEYCODE_BACK && mSelectedChip != null) {
            clearSelectedChip();
            return true;
        }
        return super.onKeyPreIme(keyCode, event);
    
public booleanonKeyUp(int keyCode, android.view.KeyEvent event)
Monitor key presses in this view to see if the user types any commit keys, which consist of ENTER, TAB, or DPAD_CENTER. If the user has entered text that has contact matches and types a commit key, create a chip from the topmost matching contact. If the user has entered text that has no contact matches and types a commit key, then create a chip from the text they have entered.

        switch (keyCode) {
            case KeyEvent.KEYCODE_TAB:
                if (event.hasNoModifiers()) {
                    if (mSelectedChip != null) {
                        clearSelectedChip();
                    } else {
                        commitDefault();
                    }
                }
                break;
        }
        return super.onKeyUp(keyCode, event);
    
public voidonLongPress(android.view.MotionEvent event)

        if (mSelectedChip != null) {
            return;
        }
        float x = event.getX();
        float y = event.getY();
        final int offset = putOffsetInRange(x, y);
        DrawableRecipientChip currentChip = findChip(offset);
        if (currentChip != null) {
            if (mDragEnabled) {
                // Start drag-and-drop for the selected chip.
                startDrag(currentChip);
            } else {
                // Copy the selected chip email address.
                showCopyDialog(currentChip.getEntry().getDestination());
            }
        }
    
public booleanonPrepareActionMode(android.view.ActionMode mode, android.view.Menu menu)

        return false;
    
public voidonRestoreInstanceState(android.os.Parcelable state)

        if (!TextUtils.isEmpty(getText())) {
            super.onRestoreInstanceState(null);
        } else {
            super.onRestoreInstanceState(state);
        }
    
public android.os.ParcelableonSaveInstanceState()

        // If the user changes orientation while they are editing, just roll back the selection.
        clearSelectedChip();
        return super.onSaveInstanceState();
    
public booleanonScroll(android.view.MotionEvent e1, android.view.MotionEvent e2, float distanceX, float distanceY)

        // Do nothing.
        return false;
    
public voidonSelectionChanged(int start, int end)

        // When selection changes, see if it is inside the chips area.
        // If so, move the cursor back after the chips again.
        DrawableRecipientChip last = getLastChip();
        if (last != null && start < getSpannable().getSpanEnd(last)) {
            // Grab the last chip and set the cursor to after it.
            setSelection(Math.min(getSpannable().getSpanEnd(last) + 1, getText().length()));
        }
        super.onSelectionChanged(start, end);
    
public voidonShowPress(android.view.MotionEvent e)

        // Do nothing.
    
public booleanonSingleTapUp(android.view.MotionEvent e)

        // Do nothing.
        return false;
    
public voidonSizeChanged(int width, int height, int oldw, int oldh)

        super.onSizeChanged(width, height, oldw, oldh);
        if (width != 0 && height != 0) {
            if (mPendingChipsCount > 0) {
                postHandlePendingChips();
            } else {
                checkChipWidths();
            }
        }
        // Try to find the scroll view parent, if it exists.
        if (mScrollView == null && !mTriedGettingScrollView) {
            ViewParent parent = getParent();
            while (parent != null && !(parent instanceof ScrollView)) {
                parent = parent.getParent();
            }
            if (parent != null) {
                mScrollView = (ScrollView) parent;
            }
            mTriedGettingScrollView = true;
        }
    
public booleanonTextContextMenuItem(int id)

        if (id == android.R.id.paste) {
            ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
                    Context.CLIPBOARD_SERVICE);
            handlePasteClip(clipboard.getPrimaryClip());
            return true;
        }
        return super.onTextContextMenuItem(id);
    
public booleanonTouchEvent(android.view.MotionEvent event)
Monitor touch events in the RecipientEditTextView. If the view does not have focus, any tap on the view will just focus the view. If the view has focus, determine if the touch target is a recipient chip. If it is and the chip is not selected, select it and clear any other selected chips. If it isn't, then select that chip.

        if (!isFocused()) {
            // Ignore any chip taps until this view is focused.
            return super.onTouchEvent(event);
        }
        boolean handled = super.onTouchEvent(event);
        int action = event.getAction();
        boolean chipWasSelected = false;
        if (mSelectedChip == null) {
            mGestureDetector.onTouchEvent(event);
        }
        if (mCopyAddress == null && action == MotionEvent.ACTION_UP) {
            float x = event.getX();
            float y = event.getY();
            int offset = putOffsetInRange(x, y);
            DrawableRecipientChip currentChip = findChip(offset);
            if (currentChip != null) {
                if (action == MotionEvent.ACTION_UP) {
                    if (mSelectedChip != null && mSelectedChip != currentChip) {
                        clearSelectedChip();
                        mSelectedChip = selectChip(currentChip);
                    } else if (mSelectedChip == null) {
                        setSelection(getText().length());
                        commitDefault();
                        mSelectedChip = selectChip(currentChip);
                    } else {
                        onClick(mSelectedChip);
                    }
                }
                chipWasSelected = true;
                handled = true;
            } else if (mSelectedChip != null && shouldShowEditableText(mSelectedChip)) {
                chipWasSelected = true;
            }
        }
        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
            clearSelectedChip();
        }
        return handled;
    
protected voidperformFiltering(java.lang.CharSequence text, int keyCode)
Instead of filtering on the entire contents of the edit box, this subclass method filters on the range from {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} if the length of that range meets or exceeds {@link #getThreshold} and makes sure that the range is not already a Chip.

        boolean isCompletedToken = isCompletedToken(text);
        if (enoughToFilter() && !isCompletedToken) {
            int end = getSelectionEnd();
            int start = mTokenizer.findTokenStart(text, end);
            // If this is a RecipientChip, don't filter
            // on its contents.
            Spannable span = getSpannable();
            DrawableRecipientChip[] chips = span.getSpans(start, end, DrawableRecipientChip.class);
            if (chips != null && chips.length > 0) {
                dismissDropDown();
                return;
            }
        } else if (isCompletedToken) {
            dismissDropDown();
            return;
        }
        super.performFiltering(text, keyCode);
    
public voidperformValidation()

        // Do nothing. Chips handles its own validation.
    
private voidpostHandlePendingChips()

        mHandler.removeCallbacks(mHandlePendingChips);
        mHandler.post(mHandlePendingChips);
    
private intputOffsetInRange(float x, float y)

        final int offset;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            offset = getOffsetForPosition(x, y);
        } else {
            offset = supportGetOffsetForPosition(x, y);
        }

        return putOffsetInRange(offset);
    
private intputOffsetInRange(int o)

        int offset = o;
        Editable text = getText();
        int length = text.length();
        // Remove whitespace from end to find "real end"
        int realLength = length;
        for (int i = length - 1; i >= 0; i--) {
            if (text.charAt(i) == ' ") {
                realLength--;
            } else {
                break;
            }
        }

        // If the offset is beyond or at the end of the text,
        // leave it alone.
        if (offset >= realLength) {
            return offset;
        }
        Editable editable = getText();
        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
            // Keep walking backward!
            offset--;
        }
        return offset;
    
voidremoveChip(com.android.ex.chips.recipientchip.DrawableRecipientChip chip)
Remove the chip and any text associated with it from the RecipientEditTextView.

        Spannable spannable = getSpannable();
        int spanStart = spannable.getSpanStart(chip);
        int spanEnd = spannable.getSpanEnd(chip);
        Editable text = getText();
        int toDelete = spanEnd;
        boolean wasSelected = chip == mSelectedChip;
        // Clear that there is a selected chip before updating any text.
        if (wasSelected) {
            mSelectedChip = null;
        }
        // Always remove trailing spaces when removing a chip.
        while (toDelete >= 0 && toDelete < text.length() && text.charAt(toDelete) == ' ") {
            toDelete++;
        }
        spannable.removeSpan(chip);
        if (spanStart >= 0 && toDelete > 0) {
            text.delete(spanStart, toDelete);
        }
        if (wasSelected) {
            clearSelectedChip();
        }
    
voidremoveMoreChip()
Replace the more chip, if it exists, with all of the recipient chips it had replaced when the RecipientEditTextView gains focus.

        if (mMoreChip != null) {
            Spannable span = getSpannable();
            span.removeSpan(mMoreChip);
            mMoreChip = null;
            // Re-add the spans that were removed.
            if (mRemovedSpans != null && mRemovedSpans.size() > 0) {
                // Recreate each removed span.
                DrawableRecipientChip[] recipients = getSortedRecipients();
                // Start the search for tokens after the last currently visible
                // chip.
                if (recipients == null || recipients.length == 0) {
                    return;
                }
                int end = span.getSpanEnd(recipients[recipients.length - 1]);
                Editable editable = getText();
                for (DrawableRecipientChip chip : mRemovedSpans) {
                    int chipStart;
                    int chipEnd;
                    String token;
                    // Need to find the location of the chip, again.
                    token = (String) chip.getOriginalText();
                    // As we find the matching recipient for the remove spans,
                    // reduce the size of the string we need to search.
                    // That way, if there are duplicates, we always find the correct
                    // recipient.
                    chipStart = editable.toString().indexOf(token, end);
                    end = chipEnd = Math.min(editable.length(), chipStart + token.length());
                    // Only set the span if we found a matching token.
                    if (chipStart != -1) {
                        editable.setSpan(chip, chipStart, chipEnd,
                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                }
                mRemovedSpans.clear();
            }
        }
    
public voidremoveRecipientEntry(RecipientEntry entry)
Remove all chips matching the given RecipientEntry.

        final DrawableRecipientChip[] recips = getText()
                .getSpans(0, getText().length(), DrawableRecipientChip.class);

        for (final DrawableRecipientChip recipient : recips) {
            final RecipientEntry existingEntry = recipient.getEntry();
            if (existingEntry != null && existingEntry.isValid() &&
                    existingEntry.isSamePerson(entry)) {
                removeChip(recipient);
            }
        }
    
public voidremoveTextChangedListener(android.text.TextWatcher watcher)

        mTextWatcher = null;
        super.removeTextChangedListener(watcher);
    
voidreplaceChip(com.android.ex.chips.recipientchip.DrawableRecipientChip chip, RecipientEntry entry)
Replace this currently selected chip with a new chip that uses the contact data provided.

        boolean wasSelected = chip == mSelectedChip;
        if (wasSelected) {
            mSelectedChip = null;
        }
        int start = getChipStart(chip);
        int end = getChipEnd(chip);
        getSpannable().removeSpan(chip);
        Editable editable = getText();
        CharSequence chipText = createChip(entry, false);
        if (chipText != null) {
            if (start == -1 || end == -1) {
                Log.e(TAG, "The chip to replace does not exist but should.");
                editable.insert(0, chipText);
            } else {
                if (!TextUtils.isEmpty(chipText)) {
                    // There may be a space to replace with this chip's new
                    // associated space. Check for it
                    int toReplace = end;
                    while (toReplace >= 0 && toReplace < editable.length()
                            && editable.charAt(toReplace) == ' ") {
                        toReplace++;
                    }
                    editable.replace(start, toReplace, chipText);
                }
            }
        }
        setCursorVisible(true);
        if (wasSelected) {
            clearSelectedChip();
        }
    
protected voidreplaceText(java.lang.CharSequence text)
We cannot use the default mechanism for replaceText. Instead, we override onItemClickListener so we can get all the associated contact information including display text, address, and id.

        return;
    
voidsanitizeBetween()

        // Don't sanitize while we are waiting for content to chipify.
        if (mPendingChipsCount > 0) {
            return;
        }
        // Find the last chip.
        DrawableRecipientChip[] recips = getSortedRecipients();
        if (recips != null && recips.length > 0) {
            DrawableRecipientChip last = recips[recips.length - 1];
            DrawableRecipientChip beforeLast = null;
            if (recips.length > 1) {
                beforeLast = recips[recips.length - 2];
            }
            int startLooking = 0;
            int end = getSpannable().getSpanStart(last);
            if (beforeLast != null) {
                startLooking = getSpannable().getSpanEnd(beforeLast);
                Editable text = getText();
                if (startLooking == -1 || startLooking > text.length() - 1) {
                    // There is nothing after this chip.
                    return;
                }
                if (text.charAt(startLooking) == ' ") {
                    startLooking++;
                }
            }
            if (startLooking >= 0 && end >= 0 && startLooking < end) {
                getText().delete(startLooking, end);
            }
        }
    
voidsanitizeEnd()
Remove any characters after the last valid chip.

        // Don't sanitize while we are waiting for pending chips to complete.
        if (mPendingChipsCount > 0) {
            return;
        }
        // Find the last chip; eliminate any commit characters after it.
        DrawableRecipientChip[] chips = getSortedRecipients();
        Spannable spannable = getSpannable();
        if (chips != null && chips.length > 0) {
            int end;
            mMoreChip = getMoreChip();
            if (mMoreChip != null) {
                end = spannable.getSpanEnd(mMoreChip);
            } else {
                end = getSpannable().getSpanEnd(getLastChip());
            }
            Editable editable = getText();
            int length = editable.length();
            if (length > end) {
                // See what characters occur after that and eliminate them.
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "There were extra characters after the last tokenizable entry."
                            + editable);
                }
                editable.delete(end + 1, length);
            }
        }
    
protected voidscrollBottomIntoView()

        if (mScrollView != null && mShouldShrink) {
            int[] location = new int[2];
            getLocationOnScreen(location);
            int height = getHeight();
            int currentPos = location[1] + height;
            // Desired position shows at least 1 line of chips below the action
            // bar. We add excess padding to make sure this is always below other
            // content.
            int desiredPos = (int) mChipHeight + mActionBarHeight + getExcessTopPadding();
            if (currentPos > desiredPos) {
                mScrollView.scrollBy(0, currentPos - desiredPos);
            }
        }
    
private voidscrollLineIntoView(int line)

        if (mScrollView != null) {
            mScrollView.smoothScrollBy(0, calculateOffsetFromBottom(line));
        }
    
private com.android.ex.chips.recipientchip.DrawableRecipientChipselectChip(com.android.ex.chips.recipientchip.DrawableRecipientChip currentChip)
Show specified chip as selected. If the RecipientChip is just an email address, selecting the chip will take the contents of the chip and place it at the end of the RecipientEditTextView for inline editing. If the RecipientChip is a complete contact, then selecting the chip will change the background color of the chip, show the delete icon, and a popup window with the address in use highlighted and any other alternate addresses for the contact.

param
currentChip Chip to select.
return
A RecipientChip in the selected state or null if the chip just contained an email address.

        if (shouldShowEditableText(currentChip)) {
            CharSequence text = currentChip.getValue();
            Editable editable = getText();
            Spannable spannable = getSpannable();
            int spanStart = spannable.getSpanStart(currentChip);
            int spanEnd = spannable.getSpanEnd(currentChip);
            spannable.removeSpan(currentChip);
            editable.delete(spanStart, spanEnd);
            setCursorVisible(true);
            setSelection(editable.length());
            editable.append(text);
            return constructChipSpan(
                    RecipientEntry.constructFakeEntry((String) text, isValid(text.toString())),
                    true);
        } else {
            int start = getChipStart(currentChip);
            int end = getChipEnd(currentChip);
            getSpannable().removeSpan(currentChip);
            DrawableRecipientChip newChip;
            final boolean showAddress =
                    currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT ||
                    getAdapter().forceShowAddress();
            try {
                if (showAddress && mNoChips) {
                    return null;
                }
                newChip = constructChipSpan(currentChip.getEntry(), true);
            } catch (NullPointerException e) {
                Log.e(TAG, e.getMessage(), e);
                return null;
            }
            Editable editable = getText();
            QwertyKeyListener.markAsReplaced(editable, start, end, "");
            if (start == -1 || end == -1) {
                Log.d(TAG, "The chip being selected no longer exists but should.");
            } else {
                editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
            newChip.setSelected(true);
            if (shouldShowEditableText(newChip)) {
                scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
            }
            if (showAddress) {
                showAddress(newChip, mAddressPopup);
            } else {
                showAlternates(newChip, mAlternatesPopup);
            }
            setCursorVisible(false);
            return newChip;
        }
    
public voidsetAdapter(T adapter)

        super.setAdapter(adapter);
        BaseRecipientAdapter baseAdapter = (BaseRecipientAdapter) adapter;
        baseAdapter.registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
            @Override
            public void onChanged(List<RecipientEntry> entries) {
                // Scroll the chips field to the top of the screen so
                // that the user can see as many results as possible.
                if (entries != null && entries.size() > 0) {
                    scrollBottomIntoView();
                }
            }
        });
        baseAdapter.setDropdownChipLayouter(mDropdownChipLayouter);
    
public voidsetAlternatePopupAnchor(android.view.View v)

        mAlternatePopupAnchor = v;
    
voidsetChipBackground(android.graphics.drawable.Drawable chipBackground)

        mChipBackground = chipBackground;
    
private voidsetChipDimensions(android.content.Context context, android.util.AttributeSet attrs)

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecipientEditTextView, 0,
                0);
        Resources r = getContext().getResources();

        mChipBackground = a.getDrawable(R.styleable.RecipientEditTextView_chipBackground);
        mChipBackgroundPressed = a
                .getDrawable(R.styleable.RecipientEditTextView_chipBackgroundPressed);
        mInvalidChipBackground = a
                .getDrawable(R.styleable.RecipientEditTextView_invalidChipBackground);
        mChipDelete = a.getDrawable(R.styleable.RecipientEditTextView_chipDelete);
        if (mChipDelete == null) {
            mChipDelete = r.getDrawable(R.drawable.ic_cancel_wht_24dp);
        }
        mChipTextStartPadding = mChipTextEndPadding
                = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipPadding, -1);
        if (mChipTextStartPadding == -1) {
            mChipTextStartPadding = mChipTextEndPadding =
                    (int) r.getDimension(R.dimen.chip_padding);
        }
        // xml-overrides for each individual padding
        // TODO: add these to attr?
        int overridePadding = (int) r.getDimension(R.dimen.chip_padding_start);
        if (overridePadding >= 0) {
            mChipTextStartPadding = overridePadding;
        }
        overridePadding = (int) r.getDimension(R.dimen.chip_padding_end);
        if (overridePadding >= 0) {
            mChipTextEndPadding = overridePadding;
        }

        mDefaultContactPhoto = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture);

        mMoreItem = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.more_item, null);

        mChipHeight = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipHeight, -1);
        if (mChipHeight == -1) {
            mChipHeight = r.getDimension(R.dimen.chip_height);
        }
        mChipFontSize = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipFontSize, -1);
        if (mChipFontSize == -1) {
            mChipFontSize = r.getDimension(R.dimen.chip_text_size);
        }
        mAvatarPosition =
                a.getInt(R.styleable.RecipientEditTextView_avatarPosition, AVATAR_POSITION_START);
        mDisableDelete = a.getBoolean(R.styleable.RecipientEditTextView_disableDelete, false);

        mMaxLines = r.getInteger(R.integer.chips_max_lines);
        mLineSpacingExtra = r.getDimensionPixelOffset(R.dimen.line_spacing_extra);
        TypedValue tv = new TypedValue();
        if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
            mActionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources()
                    .getDisplayMetrics());
        }

        a.recycle();
    
voidsetChipHeight(int height)

        mChipHeight = height;
    
public voidsetDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter)

        mDropdownChipLayouter = dropdownChipLayouter;
        mDropdownChipLayouter.setDeleteListener(this);
    
voidsetMoreItem(android.widget.TextView moreItem)

        mMoreItem = moreItem;
    
public voidsetOnFocusListShrinkRecipients(boolean shrink)
Set whether to shrink the recipients field such that at most one line of recipients chips are shown when the field loses focus. By default, the number of displayed recipients will be limited and a "more" chip will be shown when focus is lost.

param
shrink

        mShouldShrink = shrink;
    
public voidsetRecipientEntryItemClickedListener(com.android.ex.chips.RecipientEditTextView$RecipientEntryItemClickedListener listener)

        mRecipientEntryItemClickedListener = listener;
    
public voidsetTokenizer(Tokenizer tokenizer)

        mTokenizer = tokenizer;
        super.setTokenizer(mTokenizer);
    
public voidsetValidator(Validator validator)

        mValidator = validator;
        super.setValidator(validator);
    
private booleanshouldCreateChip(int start, int end)

        return !mNoChips && hasFocus() && enoughToFilter() && !alreadyHasChip(start, end);
    
private booleanshouldPositionAvatarOnRight()
Returns true if the avatar should be positioned at the right edge of the chip. Takes into account both the set avatar position (start or end) as well as whether the layout direction is LTR or RTL.

        final boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ?
                getLayoutDirection() == LAYOUT_DIRECTION_RTL : false;
        final boolean assignedPosition = mAvatarPosition == AVATAR_POSITION_END;
        // If in Rtl mode, the position should be flipped.
        return isRtl ? !assignedPosition : assignedPosition;
    
private booleanshouldShowEditableText(com.android.ex.chips.recipientchip.DrawableRecipientChip currentChip)

        long contactId = currentChip.getContactId();
        return contactId == RecipientEntry.INVALID_CONTACT
                || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
    
private voidshowAddress(com.android.ex.chips.recipientchip.DrawableRecipientChip currentChip, android.widget.ListPopupWindow popup)

        if (!mAttachedToWindow) {
            return;
        }
        int line = getLayout().getLineForOffset(getChipStart(currentChip));
        int bottomOffset = calculateOffsetFromBottomToTop(line);
        // Align the alternates popup with the left side of the View,
        // regardless of the position of the chip tapped.
        popup.setAnchorView((mAlternatePopupAnchor != null) ? mAlternatePopupAnchor : this);
        popup.setVerticalOffset(bottomOffset);
        popup.setAdapter(createSingleAddressAdapter(currentChip));
        popup.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                unselectChip(currentChip);
                popup.dismiss();
            }
        });
        popup.show();
        ListView listView = popup.getListView();
        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        listView.setItemChecked(0, true);
    
private voidshowAlternates(com.android.ex.chips.recipientchip.DrawableRecipientChip currentChip, android.widget.ListPopupWindow alternatesPopup)

        new AsyncTask<Void, Void, ListAdapter>() {
            @Override
            protected ListAdapter doInBackground(final Void... params) {
                return createAlternatesAdapter(currentChip);
            }

            @Override
            protected void onPostExecute(final ListAdapter result) {
                if (!mAttachedToWindow) {
                    return;
                }
                int line = getLayout().getLineForOffset(getChipStart(currentChip));
                int bottomOffset = calculateOffsetFromBottomToTop(line);

                // Align the alternates popup with the left side of the View,
                // regardless of the position of the chip tapped.
                alternatesPopup.setAnchorView((mAlternatePopupAnchor != null) ?
                        mAlternatePopupAnchor : RecipientEditTextView.this);
                alternatesPopup.setVerticalOffset(bottomOffset);
                alternatesPopup.setAdapter(result);
                alternatesPopup.setOnItemClickListener(mAlternatesListener);
                // Clear the checked item.
                mCheckedItem = -1;
                alternatesPopup.show();
                ListView listView = alternatesPopup.getListView();
                listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
                // Checked item would be -1 if the adapter has not
                // loaded the view that should be checked yet. The
                // variable will be set correctly when onCheckedItemChanged
                // is called in a separate thread.
                if (mCheckedItem != -1) {
                    listView.setItemChecked(mCheckedItem, true);
                    mCheckedItem = -1;
                }
            }
        }.execute((Void[]) null);
    
private voidshowCopyDialog(java.lang.String address)

        if (!mAttachedToWindow) {
            return;
        }
        mCopyAddress = address;
        mCopyDialog.setTitle(address);
        mCopyDialog.setContentView(R.layout.copy_chip_dialog_layout);
        mCopyDialog.setCancelable(true);
        mCopyDialog.setCanceledOnTouchOutside(true);
        Button button = (Button)mCopyDialog.findViewById(android.R.id.button1);
        button.setOnClickListener(this);
        int btnTitleId;
        if (isPhoneQuery()) {
            btnTitleId = R.string.copy_number;
        } else {
            btnTitleId = R.string.copy_email;
        }
        String buttonTitle = getContext().getResources().getString(btnTitleId);
        button.setText(buttonTitle);
        mCopyDialog.setOnDismissListener(this);
        mCopyDialog.show();
    
private voidshrink()

        if (mTokenizer == null) {
            return;
        }
        long contactId = mSelectedChip != null ? mSelectedChip.getEntry().getContactId() : -1;
        if (mSelectedChip != null && contactId != RecipientEntry.INVALID_CONTACT
                && (!isPhoneQuery() && contactId != RecipientEntry.GENERATED_CONTACT)) {
            clearSelectedChip();
        } else {
            if (getWidth() <= 0) {
                // We don't have the width yet which means the view hasn't been drawn yet
                // and there is no reason to attempt to commit chips yet.
                // This focus lost must be the result of an orientation change
                // or an initial rendering.
                // Re-post the shrink for later.
                mHandler.removeCallbacks(mDelayedShrink);
                mHandler.post(mDelayedShrink);
                return;
            }
            // Reset any pending chips as they would have been handled
            // when the field lost focus.
            if (mPendingChipsCount > 0) {
                postHandlePendingChips();
            } else {
                Editable editable = getText();
                int end = getSelectionEnd();
                int start = mTokenizer.findTokenStart(editable, end);
                DrawableRecipientChip[] chips =
                        getSpannable().getSpans(start, end, DrawableRecipientChip.class);
                if ((chips == null || chips.length == 0)) {
                    Editable text = getText();
                    int whatEnd = mTokenizer.findTokenEnd(text, start);
                    // This token was already tokenized, so skip past the ending token.
                    if (whatEnd < text.length() && text.charAt(whatEnd) == ',") {
                        whatEnd = movePastTerminators(whatEnd);
                    }
                    // In the middle of chip; treat this as an edit
                    // and commit the whole token.
                    int selEnd = getSelectionEnd();
                    if (whatEnd != selEnd) {
                        handleEdit(start, whatEnd);
                    } else {
                        commitChip(start, end, editable);
                    }
                }
            }
            mHandler.post(mAddTextWatcher);
        }
        createMoreChip();
    
private voidstartDrag(com.android.ex.chips.recipientchip.DrawableRecipientChip currentChip)
Starts drag-and-drop for the selected chip.

        String address = currentChip.getEntry().getDestination();
        ClipData data = ClipData.newPlainText(address, address + COMMIT_CHAR_COMMA);

        // Start drag mode.
        startDrag(data, new RecipientChipShadow(currentChip), null, 0);

        // Remove the current chip, so drag-and-drop will result in a move.
        // TODO (phamm): consider readd this chip if it's dropped outside a target.
        removeChip(currentChip);
    
private intsubmitItemAtPosition(int position)

        RecipientEntry entry = createValidatedEntry(getAdapter().getItem(position));
        if (entry == null) {
            return -1;
        }
        clearComposingText();

        int end = getSelectionEnd();
        int start = mTokenizer.findTokenStart(getText(), end);

        Editable editable = getText();
        QwertyKeyListener.markAsReplaced(editable, start, end, "");
        CharSequence chip = createChip(entry, false);
        if (chip != null && start >= 0 && end >= 0) {
            editable.replace(start, end, chip);
        }
        sanitizeBetween();

        return end - start;
    
private floatsupportConvertToLocalHorizontalCoordinate(float x)

        x -= getTotalPaddingLeft();
        // Clamp the position to inside of the view.
        x = Math.max(0.0f, x);
        x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
        x += getScrollX();
        return x;
    
private intsupportGetLineAtCoordinate(float y)

        y -= getTotalPaddingLeft();
        // Clamp the position to inside of the view.
        y = Math.max(0.0f, y);
        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
        y += getScrollY();
        return getLayout().getLineForVertical((int) y);
    
private intsupportGetOffsetAtCoordinate(int line, float x)

        x = supportConvertToLocalHorizontalCoordinate(x);
        return getLayout().getOffsetForHorizontal(line, x);
    
private intsupportGetOffsetForPosition(float x, float y)

        if (getLayout() == null) return -1;
        final int line = supportGetLineAtCoordinate(y);
        final int offset = supportGetOffsetAtCoordinate(line, x);
        return offset;
    
private static java.lang.StringtokenizeAddress(java.lang.String destination)

        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination);
        if (tokens != null && tokens.length > 0) {
            return tokens[0].getAddress();
        }
        return destination;
    
private voidunselectChip(com.android.ex.chips.recipientchip.DrawableRecipientChip chip)
Remove selection from this chip. Unselecting a RecipientChip will render the chip without a delete icon and with an unfocused background. This is called when the RecipientChip no longer has focus.

        int start = getChipStart(chip);
        int end = getChipEnd(chip);
        Editable editable = getText();
        mSelectedChip = null;
        if (start == -1 || end == -1) {
            Log.w(TAG, "The chip doesn't exist or may be a chip a user was editing");
            setSelection(editable.length());
            commitDefault();
        } else {
            getSpannable().removeSpan(chip);
            QwertyKeyListener.markAsReplaced(editable, start, end, "");
            editable.removeSpan(chip);
            try {
                if (!mNoChips) {
                    editable.setSpan(constructChipSpan(chip.getEntry(), false),
                            start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            } catch (NullPointerException e) {
                Log.e(TAG, e.getMessage(), e);
            }
        }
        setCursorVisible(true);
        setSelection(editable.length());
        if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
            mAlternatesPopup.dismiss();
        }