/*

 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.ex.chips;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.text.Editable;
import android.text.InputType;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.QwertyKeyListener;
import android.text.style.ImageSpan;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.ActionMode;
import android.view.ActionMode.Callback;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewParent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.Button;
import android.widget.Filterable;
import android.widget.ListAdapter;
import android.widget.ListPopupWindow;
import android.widget.ListView;
import android.widget.MultiAutoCompleteTextView;
import android.widget.ScrollView;
import android.widget.TextView;
import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
import com.android.ex.chips.recipientchip.DrawableRecipientChip;
import com.android.ex.chips.recipientchip.InvisibleRecipientChip;
import com.android.ex.chips.recipientchip.VisibleRecipientChip;

/**
 * RecipientEditTextView is an auto complete text view for use with applications that use the new Chips UI for
 * addressing a message to recipients.
 */
public class RecipientEditTextView extends MultiAutoCompleteTextView implements Callback,RecipientAlternatesAdapter.OnCheckedItemChangedListener,GestureDetector.OnGestureListener,OnDismissListener,OnClickListener,TextView.OnEditorActionListener
  {
  /** max pixels till touching up/down out of the initial touch coordinate is considered as scrolling */
  private static final int                 SCROLLING_BOUNDS_THRESHOLD_IN_PIXELS =2;
  /**
   * This pattern comes from android.util.Patterns. It has been tweaked to handle a "1" before parens, so numbers such
   * as "1 (425) 222-2342" match.
   */
  private static final Pattern             PHONE_PATTERN                        =Pattern.compile( // sdd = space, dot, or dash
                                                                                    "(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>*
                                                                                        +"(1?[ ]*\\([0-9]+\\)[\\- \\.]*)?" // 1(<digits>)<sdd>*
                                                                                        +"([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])");                   // <digit><digit|sdd>+<digit>
  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                            =String.valueOf(COMMIT_CHAR_COMMA)+String.valueOf(COMMIT_CHAR_SPACE);
  private static final String              TAG                                  ="RecipientEditTextView";
  private static final int                 DISMISS                              ="dismiss".hashCode();
  private static final long                DISMISS_DELAY                        =300;
  // TODO: get correct number/ algorithm from with UX.
  // Visible for testing.
  /* package */static final int            CHIP_LIMIT                           =2;
  private static final int                 MAX_CHIPS_PARSED                     =50;
  private static int                       sSelectedTextColor                   =-1;
  // Resources for displaying chips.
  private Drawable                         mChipBackground                      =null;
  private Drawable                         mChipDelete                          =null;
  private Drawable                         mInvalidChipBackground;
  private Drawable                         mChipBackgroundPressed;
  private float                            mChipHeight;
  private float                            mChipFontSize;
  private float                            mLineSpacingExtra;
  private int                              mChipPadding;
  private Tokenizer                        mTokenizer;
  private Validator                        mValidator;
  private DrawableRecipientChip            mSelectedChip;
  private int                              mAlternatesLayout;
  private Bitmap                           mDefaultContactPhoto;
  private ImageSpan                        mMoreChip;
  private TextView                         mMoreItem;
  // VisibleForTesting
  private final ArrayList<String>          mPendingChips                        =new ArrayList<String>();
  private final Handler                    mHandler;
  private int                              mPendingChipsCount                   =0;
  private boolean                          mNoChips                             =false;
  private ListPopupWindow                  mAlternatesPopup,mAddressPopup;
  // VisibleForTesting
  private ArrayList<DrawableRecipientChip> mTemporaryRecipients;
  private ArrayList<DrawableRecipientChip> mRemovedSpans;
  private boolean                          mShouldShrink                        =true;
  // Chip copy fields.
  private GestureDetector                  mGestureDetector;
  private final Dialog                     mCopyDialog;
  private String                           mCopyAddress;
  /** Used with {@link #mAlternatesPopup}. Handles clicks to alternate addresses for a selected chip. */
  private OnItemClickListener              mAlternatesListener;
  private int                              mCheckedItem;
  private TextWatcher                      mTextWatcher;
  /**
   * Obtain the enclosing scroll view, if it exists, so that the view can be scrolled to show the last line of chips
   * content.
   */
  private ScrollView                       mScrollView;
  private boolean                          mTriedGettingScrollView;
  private int                              mMaxLines;
  private static int                       sExcessTopPadding                    =-1;
  private int                              mActionBarHeight;
  private boolean                          mAttachedToWindow;
  private final Runnable                   mAddTextWatcher;
  private final Runnable                   mHandlePendingChips;
  private final Runnable                   mDelayedShrink;
  private IndividualReplacementTask        mIndividualReplacements;
  private IChipListener                    mChipListener;
  private int                              mPreviousChipsCount                  =0;
  private final EnumSet<FocusBehavior>     mFocusBehavior                       =EnumSet.allOf(FocusBehavior.class);
  /** used to store initial touch Y coordinate, in order to identify scrolling */
  private int                              mStartTouchY                         =-1;
  private boolean                          mIsScrolling                         =false;

  public enum FocusBehavior
    {
    SHRINK_WHEN_LOST_FOCUS,EXPAND_WHEN_GOT_FOCUS
    }

  public interface IChipListener
    {
    void onDataChanged();
    }

  public RecipientEditTextView(final Context context,final AttributeSet attrs)
    {
    super(context,attrs);
    mAddTextWatcher=new Runnable()
      {
        @Override
        public void run()
          {
          if(mTextWatcher==null)
            {
            mTextWatcher=new RecipientTextWatcher();
            addTextChangedListener(mTextWatcher);
            }
          }
      };
    mDelayedShrink=new Runnable()
      {
        @Override
        public void run()
          {
          shrink();
          }
      };
    mHandlePendingChips=new Runnable()
      {
        @Override
        public void run()
          {
          handlePendingChips();
          }
      };
    setChipDimensions(context,attrs);
    if(sSelectedTextColor==-1)
      sSelectedTextColor=context.getResources().getColor(android.R.color.white);
    mAlternatesPopup=new ListPopupWindow(context);
    mAddressPopup=new ListPopupWindow(context);
    mCopyDialog=new Dialog(context);
    mAlternatesListener=new OnItemClickListener()
      {
        @Override
        public void onItemClick(final AdapterView<?> adapterView,final View view,final int position,final long rowId)
          {
          mAlternatesPopup.setOnItemClickListener(null);
          replaceChip(mSelectedChip,((RecipientAlternatesAdapter)adapterView.getAdapter()).getRecipientEntry(position));
          final Message delayed=Message.obtain(mHandler,DISMISS);
          delayed.obj=mAlternatesPopup;
          mHandler.sendMessageDelayed(delayed,DISMISS_DELAY);
          clearComposingText();
          }
      };
    setInputType(getInputType()|InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    setOnItemClickListener(null);
    setCustomSelectionActionModeCallback(this);
    mHandler=new Handler()
      {
        @Override
        public void handleMessage(final 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);
    }

  @Override
  public void setOnItemClickListener(final OnItemClickListener listener)
    {
    super.setOnItemClickListener(new OnItemClickListener()
      {
        @Override
        public void onItemClick(final AdapterView<?> parent,final View view,final int position,final 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;
          submitItemAtPosition(position);
          if(listener!=null)
            listener.onItemClick(parent,view,position,id);
          }
      });
    }

  @Override
  protected void onDetachedFromWindow()
    {
    mAttachedToWindow=false;
    super.onDetachedFromWindow();
    }

  @Override
  protected void onAttachedToWindow()
    {
    mAttachedToWindow=true;
    }

  @Override
  public boolean onEditorAction(final TextView view,final int action,final 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;
    }

  @Override
  public InputConnection onCreateInputConnection(final EditorInfo outAttrs)
    {
    final InputConnection connection=super.onCreateInputConnection(outAttrs);
    final 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.done);
    return connection;
    }

  /* package */DrawableRecipientChip getLastChip()
    {
    DrawableRecipientChip last=null;
    final DrawableRecipientChip[] chips=getSortedRecipients();
    if(chips!=null&&chips.length>0)
      last=chips[chips.length-1];
    return last;
    }

  @Override
  public void onSelectionChanged(final int start,final int end)
    {
    // When selection changes, see if it is inside the chips area.
    // If so, move the cursor back after the chips again.
    final 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);
    }

  @Override
  public void onRestoreInstanceState(final Parcelable state)
    {
    if(!TextUtils.isEmpty(getText()))
      super.onRestoreInstanceState(null);
    else super.onRestoreInstanceState(state);
    }

  @Override
  public Parcelable onSaveInstanceState()
    {
    // If the user changes orientation while they are editing, just roll back the selection.
    clearSelectedChip();
    return super.onSaveInstanceState();
    }

  /**
   * 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.
   */
  @Override
  public void append(final CharSequence text,final int start,final int end)
    {
    // 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);
    }

  @Override
  public void onFocusChanged(final boolean hasFocus,final int direction,final Rect previous)
    {
    super.onFocusChanged(hasFocus,direction,previous);
    if(!hasFocus)
      {
      if(mFocusBehavior.contains(FocusBehavior.SHRINK_WHEN_LOST_FOCUS))
        shrink();
      }
    else if(mFocusBehavior.contains(FocusBehavior.EXPAND_WHEN_GOT_FOCUS))
      expand();
    }

  private int getExcessTopPadding()
    {
    if(sExcessTopPadding==-1)
      sExcessTopPadding=(int)(mChipHeight+mLineSpacingExtra);
    return sExcessTopPadding;
    }

  @Override
  public <T extends ListAdapter & Filterable> void setAdapter(final T adapter)
    {
    super.setAdapter(adapter);
    ((BaseRecipientAdapter)adapter).registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver()
      {
        @Override
        public void onChanged(final 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();
          }
      });
    }

  private void scrollBottomIntoView()
    {
    if(mScrollView!=null&&mShouldShrink)
      {
      final int[] location=new int[2];
      getLocationOnScreen(location);
      final int height=getHeight();
      final 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.
      final int desiredPos=(int)mChipHeight+mActionBarHeight+getExcessTopPadding();
      if(currentPos>desiredPos)
        mScrollView.scrollBy(0,currentPos-desiredPos);
      }
    }

  @Override
  public void performValidation()
    {
    // Do nothing. Chips handles its own validation.
    }

  private void shrink()
    {
    if(mTokenizer==null)
      return;
    final 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
        {
        final Editable editable=getText();
        final int end=getSelectionEnd();
        final int start=mTokenizer.findTokenStart(editable,end);
        final DrawableRecipientChip[] chips=getSpannable().getSpans(start,end,DrawableRecipientChip.class);
        if(chips==null||chips.length==0&&start>=0)
          {
          final Editable text=getText();
          // TODO check why this code can crash (index out of bounds). Currently fixed by checking that
          // start>=0
          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.
          final int selEnd=getSelectionEnd();
          if(whatEnd!=selEnd)
            handleEdit(start,whatEnd);
          else commitChip(start,end,editable);
          }
        }
      mHandler.post(mAddTextWatcher);
      }
    createMoreChip();
    }

  private void expand()
    {
    if(mShouldShrink)
      setMaxLines(Integer.MAX_VALUE);
    removeMoreChip();
    setCursorVisible(true);
    final 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 CharSequence ellipsizeText(final CharSequence text,final TextPaint paint,final float maxWidth)
    {
    paint.setTextSize(mChipFontSize);
    if(maxWidth<=0&&Log.isLoggable(TAG,Log.DEBUG))
      Log.d(TAG,"Max width is negative: "+maxWidth);
    final CharSequence ellipsize=TextUtils.ellipsize(text,paint,maxWidth,TextUtils.TruncateAt.END);
    return ellipsize;
    }

  private Bitmap createSelectedChip(final RecipientEntry contact,final TextPaint paint)
    {
    // 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.
    final int height=(int)mChipHeight;
    final int deleteWidth=height;
    final float[] widths=new float[1];
    paint.getTextWidths(" ",widths);
    final String createChipDisplayText=createChipDisplayText(contact);
    final float calculateAvailableWidth=calculateAvailableWidth();
    final CharSequence ellipsizedText=ellipsizeText(createChipDisplayText,paint,calculateAvailableWidth-deleteWidth-widths[0]);
    // Make sure there is a minimum chip width so the user can ALWAYS
    // tap a chip without difficulty.
    final int width=Math.max(deleteWidth*2,(int)Math.floor(paint.measureText(ellipsizedText,0,ellipsizedText.length()))+mChipPadding*2+deleteWidth);
    // Create the background of the chip.
    final Bitmap tmpBitmap=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
    final Canvas canvas=new Canvas(tmpBitmap);
    if(mChipBackgroundPressed!=null)
      {
      mChipBackgroundPressed.setBounds(0,0,width,height);
      mChipBackgroundPressed.draw(canvas);
      paint.setColor(sSelectedTextColor);
      // Vertically center the text in the chip.
      canvas.drawText(ellipsizedText,0,ellipsizedText.length(),mChipPadding,getTextYOffset((String)ellipsizedText,paint,height),paint);
      // Make the delete a square.
      final Rect backgroundPadding=new Rect();
      mChipBackgroundPressed.getPadding(backgroundPadding);
      mChipDelete.setBounds(width-deleteWidth+backgroundPadding.left,0+backgroundPadding.top,width-backgroundPadding.right,height-backgroundPadding.bottom);
      mChipDelete.draw(canvas);
      }
    else Log.w(TAG,"Unable to draw a background for the chips as it was never set");
    return tmpBitmap;
    }

  private Bitmap createUnselectedChip(final RecipientEntry contact,final TextPaint paint,final boolean leaveBlankIconSpacer)
    {
    // 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.
    final int height=(int)mChipHeight;
    int iconWidth=height;
    final float[] widths=new float[1];
    paint.getTextWidths(" ",widths);
    final float availableWidth=calculateAvailableWidth();
    final String chipDisplayText=createChipDisplayText(contact);
    final CharSequence ellipsizedText=ellipsizeText(chipDisplayText,paint,availableWidth-iconWidth-widths[0]);
    // Make sure there is a minimum chip width so the user can ALWAYS
    // tap a chip without difficulty.
    final int width=Math.max(iconWidth*2,(int)Math.floor(paint.measureText(ellipsizedText,0,ellipsizedText.length()))+mChipPadding*2+iconWidth);
    // Create the background of the chip.
    final Bitmap tmpBitmap=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
    final Canvas canvas=new Canvas(tmpBitmap);
    final Drawable background=getChipBackground(contact);
    if(background!=null)
      {
      background.setBounds(0,0,width,height);
      background.draw(canvas);
      // Don't draw photos for recipients that have been typed in OR generated on the fly.
      final long contactId=contact.getContactId();
      final boolean drawPhotos=isPhoneQuery() ? contactId!=RecipientEntry.INVALID_CONTACT : contactId!=RecipientEntry.INVALID_CONTACT&&contactId!=RecipientEntry.GENERATED_CONTACT&&!TextUtils.isEmpty(contact.getDisplayName());
      if(drawPhotos)
        {
        byte[] photoBytes=contact.getPhotoBytes();
        // There may not be a photo yet if anything but the first contact address
        // was selected.
        if(photoBytes==null&&contact.getPhotoThumbnailUri()!=null)
          {
          // TODO: cache this in the recipient entry?
          getAdapter().fetchPhoto(contact,contact.getPhotoThumbnailUri());
          photoBytes=contact.getPhotoBytes();
          }
        Bitmap photo;
        if(photoBytes!=null)
          photo=BitmapFactory.decodeByteArray(photoBytes,0,photoBytes.length);
        else // TODO: can the scaled down default photo be cached?
        photo=mDefaultContactPhoto;
        // Draw the photo on the left side.
        if(photo!=null)
          {
          final RectF src=new RectF(0,0,photo.getWidth(),photo.getHeight());
          final Rect backgroundPadding=new Rect();
          mChipBackground.getPadding(backgroundPadding);
          final RectF dst=new RectF(width-iconWidth+backgroundPadding.left,0+backgroundPadding.top,width-backgroundPadding.right,height-backgroundPadding.bottom);
          final Matrix matrix=new Matrix();
          matrix.setRectToRect(src,dst,Matrix.ScaleToFit.FILL);
          canvas.drawBitmap(photo,matrix,paint);
          }
        }
      else if(!leaveBlankIconSpacer||isPhoneQuery())
        iconWidth=0;
      paint.setColor(getContext().getResources().getColor(android.R.color.black));
      // Vertically center the text in the chip.
      canvas.drawText(ellipsizedText,0,ellipsizedText.length(),mChipPadding,getTextYOffset((String)ellipsizedText,paint,height),paint);
      }
    else Log.w(TAG,"Unable to draw a background for the chips as it was never set");
    return tmpBitmap;
    }

  /**
   * Get the background drawable for a RecipientChip.
   */
  // Visible for testing.
  /* package */Drawable getChipBackground(final RecipientEntry contact)
    {
    return contact.isValid() ? mChipBackground : mInvalidChipBackground;
    }

  private static float getTextYOffset(final String text,final TextPaint paint,final int height)
    {
    final Rect bounds=new Rect();
    paint.getTextBounds(text,0,text.length(),bounds);
    final int textHeight=bounds.bottom-bounds.top;
    return height-(height-textHeight)/2-(int)paint.descent();
    }

  private DrawableRecipientChip constructChipSpan(final RecipientEntry contact,final boolean pressed,final boolean leaveIconSpace) throws NullPointerException
    {
    if(mChipBackground==null)
      throw new NullPointerException("Unable to render any chips as setChipDimensions was not called.");
    final TextPaint paint=getPaint();
    final float defaultSize=paint.getTextSize();
    final int defaultColor=paint.getColor();
    Bitmap tmpBitmap;
    if(pressed)
      tmpBitmap=createSelectedChip(contact,paint);
    else tmpBitmap=createUnselectedChip(contact,paint,leaveIconSpace);
    // Pass the full text, un-ellipsized, to the chip.
    final Drawable result=new BitmapDrawable(getResources(),tmpBitmap);
    result.setBounds(0,0,tmpBitmap.getWidth(),tmpBitmap.getHeight());
    final DrawableRecipientChip recipientChip=new VisibleRecipientChip(result,contact);
    // Return text to the original size.
    paint.setTextSize(defaultSize);
    paint.setColor(defaultColor);
    return recipientChip;
    }

  /**
   * 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
   */
  private int calculateOffsetFromBottom(final int line)
    {
    // Line offsets start at zero.
    final int actualLine=getLineCount()-(line+1);
    return -(actualLine*(int)mChipHeight+getPaddingBottom()+getPaddingTop())+getDropDownVerticalOffset();
    }

  /**
   * 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.
   */
  private float calculateAvailableWidth()
    {
    return getWidth()-getPaddingLeft()-getPaddingRight()-mChipPadding*2;
    }

  private void setChipDimensions(final Context context,final AttributeSet attrs)
    {
    final TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.RecipientEditTextView,0,0);
    final Resources r=getContext().getResources();
    mChipBackground=a.getDrawable(R.styleable.RecipientEditTextView_chipBackground);
    if(mChipBackground==null)
      mChipBackground=r.getDrawable(R.drawable.chip_background);
    mChipBackgroundPressed=a.getDrawable(R.styleable.RecipientEditTextView_chipBackgroundPressed);
    if(mChipBackgroundPressed==null)
      mChipBackgroundPressed=r.getDrawable(R.drawable.chip_background_selected);
    mChipDelete=a.getDrawable(R.styleable.RecipientEditTextView_chipDelete);
    if(mChipDelete==null)
      mChipDelete=r.getDrawable(R.drawable.chip_delete);
    mChipPadding=a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipPadding,-1);
    if(mChipPadding==-1)
      mChipPadding=(int)r.getDimension(R.dimen.chip_padding);
    mAlternatesLayout=a.getResourceId(R.styleable.RecipientEditTextView_chipAlternatesLayout,-1);
    if(mAlternatesLayout==-1)
      mAlternatesLayout=R.layout.chips_alternate_item;
    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);
    mInvalidChipBackground=a.getDrawable(R.styleable.RecipientEditTextView_invalidChipBackground);
    if(mInvalidChipBackground==null)
      mInvalidChipBackground=r.getDrawable(R.drawable.chip_background_invalid);
    mLineSpacingExtra=r.getDimension(R.dimen.line_spacing_extra);
    mMaxLines=r.getInteger(R.integer.chips_max_lines);
    final TypedValue tv=new TypedValue();
    if(context.getTheme().resolveAttribute(android.R.attr.actionBarSize,tv,true))
      mActionBarHeight=TypedValue.complexToDimensionPixelSize(tv.data,getResources().getDisplayMetrics());
    a.recycle();
    }

  // Visible for testing.
  /* package */void setMoreItem(final TextView moreItem)
    {
    mMoreItem=moreItem;
    }

  // Visible for testing.
  /* package */void setChipBackground(final Drawable chipBackground)
    {
    mChipBackground=chipBackground;
    }

  // Visible for testing.
  /* package */void setChipHeight(final int height)
    {
    mChipHeight=height;
    }

  /**
   * 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
   */
  public void setOnFocusListShrinkRecipients(final boolean shrink)
    {
    mShouldShrink=shrink;
    }

  @Override
  public void onSizeChanged(final int width,final int height,final int oldw,final 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;
      }
    }

  private void postHandlePendingChips()
    {
    mHandler.removeCallbacks(mHandlePendingChips);
    mHandler.post(mHandlePendingChips);
    }

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

  // Visible for testing.
  /* package */void handlePendingChips()
    {
    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)
      {
      final Editable editable=getText();
      // Tokenize!
      if(mPendingChipsCount<=MAX_CHIPS_PARSED)
        {
        for(int i=0;i<mPendingChips.size();i++)
          {
          final String current=mPendingChips.get(i);
          final 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();
      }
    }

  // Visible for testing.
  /* package */int getViewWidth()
    {
    return getWidth();
    }

  /**
   * Remove any characters after the last valid chip.
   */
  // Visible for testing.
  /* package */void sanitizeEnd()
    {
    // 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.
    final DrawableRecipientChip[] chips=getSortedRecipients();
    final 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());
      final Editable editable=getText();
      final 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);
        }
      }
    }

  /**
   * 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.
   */
  // VisibleForTesting
  void createReplacementChip(final int tokenStart,final int tokenEnd,final Editable editable,final boolean visible)
    {
    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();
    final int commitCharIndex=trimmedToken.lastIndexOf(COMMIT_CHAR_COMMA);
    if(commitCharIndex!=-1&&commitCharIndex==trimmedToken.length()-1)
      token=trimmedToken.substring(0,trimmedToken.length()-1);
    final RecipientEntry entry=createTokenizedEntry(token);
    if(entry!=null)
      {
      DrawableRecipientChip chip=null;
      try
        {
        if(!mNoChips)
          {
          /*
           * leave space for the contact icon if this is not just an email address
           */
          final boolean leaveSpace=TextUtils.isEmpty(entry.getDisplayName())||TextUtils.equals(entry.getDisplayName(),entry.getDestination());
          chip=visible ? constructChipSpan(entry,false,leaveSpace) : new InvisibleRecipientChip(entry);
          }
        }
      catch(final NullPointerException e)
        {
        Log.e(TAG,e.getMessage(),e);
        }
      editable.setSpan(chip,tokenStart,tokenEnd,Spanned.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 static boolean isPhoneNumber(final 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;
    final Matcher match=PHONE_PATTERN.matcher(number);
    return match.matches();
    }

  // VisibleForTesting
  RecipientEntry createTokenizedEntry(final String token)
    {
    if(TextUtils.isEmpty(token))
      return null;
    if(isPhoneQuery()&&isPhoneNumber(token))
      return RecipientEntry.constructFakePhoneEntry(token,true);
    final 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
          final 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 boolean isValid(final String text)
    {
    return mValidator==null ? true : mValidator.isValid(text);
    }

  private static String tokenizeAddress(final String destination)
    {
    final Rfc822Token[] tokens=Rfc822Tokenizer.tokenize(destination);
    if(tokens!=null&&tokens.length>0)
      return tokens[0].getAddress();
    return destination;
    }

  @Override
  public void setTokenizer(final Tokenizer tokenizer)
    {
    mTokenizer=tokenizer;
    super.setTokenizer(mTokenizer);
    }

  @Override
  public void setValidator(final Validator validator)
    {
    mValidator=validator;
    super.setValidator(validator);
    }

  /**
   * 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.
   */
  @Override
  protected void replaceText(final CharSequence text)
    {
    return;
    }

  /**
   * Dismiss any selected chips when the back key is pressed.
   */
  @Override
  public boolean onKeyPreIme(final int keyCode,final KeyEvent event)
    {
    if(keyCode==KeyEvent.KEYCODE_BACK&&mSelectedChip!=null)
      {
      clearSelectedChip();
      return true;
      }
    return super.onKeyPreIme(keyCode,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.
   */
  @Override
  public boolean onKeyUp(final int keyCode,final KeyEvent event)
    {
    switch(keyCode)
      {
      case KeyEvent.KEYCODE_TAB:
        if(event.hasNoModifiers())
          if(mSelectedChip!=null)
            clearSelectedChip();
          else commitDefault();
        break;
      }
    return super.onKeyUp(keyCode,event);
    }

  private boolean focusNext()
    {
    final View next=focusSearch(View.FOCUS_DOWN);
    if(next!=null)
      {
      next.requestFocus();
      return true;
      }
    return false;
    }

  /**
   * Create a chip from the default selection. If the popup is showing, the default is the first item in the popup
   * suggestions list. Otherwise, it is whatever the user had typed in. End represents where the the tokenizer should
   * search for a token to turn into a chip.
   *
   * @return If a chip was created from a real contact.
   */
  private boolean commitDefault()
    {
    // If there is no tokenizer, don't try to commit.
    if(mTokenizer==null)
      return false;
    final Editable editable=getText();
    final int end=getSelectionEnd();
    final 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 void commitByCharacter()
    {
    // We can't possibly commit by character if we can't tokenize.
    if(mTokenizer==null)
      return;
    final Editable editable=getText();
    final int end=getSelectionEnd();
    final int start=mTokenizer.findTokenStart(editable,end);
    if(shouldCreateChip(start,end))
      commitChip(start,end,editable);
    setSelection(getText().length());
    }

  private boolean commitChip(final int start,final int end,final Editable editable)
    {
    final ListAdapter adapter=getAdapter();
    if(adapter!=null&&adapter.getCount()>0&&enoughToFilter()&&end==getSelectionEnd()&&!isPhoneQuery())
      {
      // choose the first entry.
      submitItemAtPosition(0);
      dismissDropDown();
      return true;
      }
    else
      {
      int tokenEnd=mTokenizer.findTokenEnd(editable,start);
      if(editable.length()>tokenEnd+1)
        {
        final char charAt=editable.charAt(tokenEnd+1);
        if(charAt==COMMIT_CHAR_COMMA||charAt==COMMIT_CHAR_SEMICOLON)
          tokenEnd++;
        }
      final String text=editable.toString().substring(start,tokenEnd).trim();
      clearComposingText();
      if(text!=null&&text.length()>0&&!text.equals(" "))
        {
        final RecipientEntry entry=createTokenizedEntry(text);
        if(entry!=null)
          {
          QwertyKeyListener.markAsReplaced(editable,start,end,"");
          final 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;
    }

  // Visible for testing.
  /* package */void sanitizeBetween()
    {
    // Don't sanitize while we are waiting for content to chipify.
    if(mPendingChipsCount>0)
      return;
    // Find the last chip.
    final DrawableRecipientChip[] recips=getSortedRecipients();
    if(recips!=null&&recips.length>0)
      {
      final DrawableRecipientChip last=recips[recips.length-1];
      DrawableRecipientChip beforeLast=null;
      if(recips.length>1)
        beforeLast=recips[recips.length-2];
      int startLooking=0;
      final int end=getSpannable().getSpanStart(last);
      if(beforeLast!=null)
        {
        startLooking=getSpannable().getSpanEnd(beforeLast);
        final 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);
      }
    }

  private boolean shouldCreateChip(final int start,final int end)
    {
    return !mNoChips&&hasFocus()&&enoughToFilter()&&!alreadyHasChip(start,end);
    }

  private boolean alreadyHasChip(final int start,final int end)
    {
    if(mNoChips)
      return true;
    final DrawableRecipientChip[] chips=getSpannable().getSpans(start,end,DrawableRecipientChip.class);
    if(chips==null||chips.length==0)
      return false;
    return true;
    }

  private void handleEdit(final int start,final 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.
    final Editable editable=getText();
    setSelection(end);
    final String text=getText().toString().substring(start,end);
    if(!TextUtils.isEmpty(text))
      {
      final RecipientEntry entry=RecipientEntry.constructFakeEntry(text,isValid(text));
      QwertyKeyListener.markAsReplaced(editable,start,end,"");
      final CharSequence chipText=createChip(entry,false);
      final int selEnd=getSelectionEnd();
      if(chipText!=null&&start>-1&&selEnd>-1)
        editable.replace(start,selEnd,chipText);
      }
    dismissDropDown();
    }

  /**
   * If there is a selected chip, delegate the key events to the selected chip.
   */
  @Override
  public boolean onKeyDown(final int keyCode,final KeyEvent event)
    {
    if(mSelectedChip!=null&&keyCode==KeyEvent.KEYCODE_DEL)
      {
      if(mAlternatesPopup!=null&&mAlternatesPopup.isShowing())
        mAlternatesPopup.dismiss();
      removeChip(mSelectedChip,true);
      }
    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);
    }

  // Visible for testing.
  /* package */Spannable getSpannable()
    {
    return getText();
    }

  private int getChipStart(final DrawableRecipientChip chip)
    {
    return getSpannable().getSpanStart(chip);
    }

  private int getChipEnd(final DrawableRecipientChip chip)
    {
    return getSpannable().getSpanEnd(chip);
    }

  /**
   * 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.
   */
  @Override
  protected void performFiltering(final CharSequence text,final int keyCode)
    {
    final boolean isCompletedToken=isCompletedToken(text);
    if(enoughToFilter()&&!isCompletedToken)
      {
      final int end=getSelectionEnd();
      final int start=mTokenizer.findTokenStart(text,end);
      // If this is a RecipientChip, don't filter
      // on its contents.
      final Spannable span=getSpannable();
      final DrawableRecipientChip[] chips=span.getSpans(start,end,DrawableRecipientChip.class);
      if(chips!=null&&chips.length>0)
        return;
      }
    else if(isCompletedToken)
      return;
    super.performFiltering(text,keyCode);
    }

  // Visible for testing.
  /* package */boolean isCompletedToken(final CharSequence text)
    {
    if(TextUtils.isEmpty(text))
      return false;
    // Check to see if this is a completed token before filtering.
    final int end=text.length();
    final int start=mTokenizer.findTokenStart(text,end);
    final String token=text.toString().substring(start,end).trim();
    if(!TextUtils.isEmpty(token))
      {
      final char atEnd=token.charAt(token.length()-1);
      return atEnd==COMMIT_CHAR_COMMA||atEnd==COMMIT_CHAR_SEMICOLON;
      }
    return false;
    }

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

  /**
   * 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.
   */
  @Override
  public boolean onTouchEvent(final MotionEvent event)
    {
    if(!isFocused())
      // Ignore any chip taps until this view is focused.
      return super.onTouchEvent(event);
    android.util.Log.d("AppLog","scroll:"+getScrollY());
    boolean handled=super.onTouchEvent(event);
    final int action=event.getAction();
    boolean chipWasSelected=false;
    if(mSelectedChip==null)
      mGestureDetector.onTouchEvent(event);
    switch(action)
      {
      case MotionEvent.ACTION_DOWN:
        mIsScrolling=false;
        mStartTouchY=(int)event.getY();
        break;
      case MotionEvent.ACTION_MOVE:
        if(Math.abs(event.getY()-mStartTouchY)>SCROLLING_BOUNDS_THRESHOLD_IN_PIXELS)
          mIsScrolling=true;
        break;
      case MotionEvent.ACTION_UP:
        if(mIsScrolling)
          {
          mIsScrolling=false;
          break;
          }
        if(mCopyAddress==null)
          {
          final float x=event.getX();
          final float y=event.getY();
          final int offset=putOffsetInRange(x,y);
          final 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,offset,x,y);
            chipWasSelected=true;
            handled=true;
            }
          else if(mSelectedChip!=null&&shouldShowEditableText(mSelectedChip))
            chipWasSelected=true;
          }
        if(!chipWasSelected)
          clearSelectedChip();
        break;
      }
    return handled;
    }

  private void scrollLineIntoView(final int line)
    {
    if(mScrollView!=null)
      mScrollView.smoothScrollBy(0,calculateOffsetFromBottom(line));
    }

  private void showAlternates(final DrawableRecipientChip currentChip,final ListPopupWindow alternatesPopup,final int width)
    {
    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;
          final int line=getLayout().getLineForOffset(getChipStart(currentChip));
          int bottom;
          if(line==getLineCount()-1)
            bottom=0;
          else bottom=-(int)((mChipHeight+2*mLineSpacingExtra)*Math.abs(getLineCount()-1-line));
          // Align the alternates popup with the left side of the View,
          // regardless of the position of the chip tapped.
          alternatesPopup.setWidth(width);
          alternatesPopup.setAnchorView(RecipientEditTextView.this);
          alternatesPopup.setVerticalOffset(bottom);
          alternatesPopup.setAdapter(result);
          alternatesPopup.setOnItemClickListener(mAlternatesListener);
          // Clear the checked item.
          mCheckedItem=-1;
          alternatesPopup.show();
          final ListView listView=alternatesPopup.getListView();
          listView.setChoiceMode(AbsListView.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);
    }

  protected ListAdapter createAlternatesAdapter(final DrawableRecipientChip chip)
    {
    return new RecipientAlternatesAdapter(getContext(),chip.getContactId(),chip.getDataId(),getAdapter().getQueryType(),this);
    }

  private ListAdapter createSingleAddressAdapter(final DrawableRecipientChip currentChip)
    {
    return new SingleRecipientArrayAdapter(getContext(),mAlternatesLayout,currentChip.getEntry());
    }

  @Override
  public void onCheckedItemChanged(final int position)
    {
    final ListView listView=mAlternatesPopup.getListView();
    if(listView!=null&&listView.getCheckedItemCount()==0)
      listView.setItemChecked(position,true);
    mCheckedItem=position;
    }

  @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
  private int putOffsetInRange(final float x,final 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);
    }

  // TODO: This algorithm will need a lot of tweaking after more people have used
  // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
  // what comes before the finger.
  private int putOffsetInRange(final int o)
    {
    int offset=o;
    final Editable text=getText();
    final 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;
    final Editable editable=getText();
    while(offset>=0&&findText(editable,offset)==-1&&findChip(offset)==null)
      // Keep walking backward!
      offset--;
    return offset;
    }

  private static int findText(final Editable text,final int offset)
    {
    if(text.charAt(offset)!=' ')
      return offset;
    return -1;
    }

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

  // Visible for testing.
  // Use this method to generate text to add to the list of addresses.
  /* package */String createAddressText(final 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.
        final Rfc822Token[] tokenized=Rfc822Tokenizer.tokenize(address);
        if(tokenized!=null&&tokenized.length>0)
          address=tokenized[0].getAddress();
        }
      final Rfc822Token token=new Rfc822Token(display,address,null);
      trimmedDisplayText=token.toString().trim();
      }
    final int index=trimmedDisplayText.indexOf(",");
    return mTokenizer!=null&&!TextUtils.isEmpty(trimmedDisplayText)&&index<trimmedDisplayText.length()-1 ? (String)mTokenizer.terminateToken(trimmedDisplayText) : trimmedDisplayText;
    }

  // Visible for testing.
  // Use this method to generate text to display in a chip.
  /* package */String createChipDisplayText(final RecipientEntry entry)
    {
    String display=entry.getDisplayName();
    final 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();
    }

  private CharSequence createChip(final RecipientEntry entry,final boolean pressed)
    {
    final String displayText=createAddressText(entry);
    if(TextUtils.isEmpty(displayText))
      return null;
    SpannableString chipText=null;
    // Always leave a blank space at the end of a chip.
    final int textLength=displayText.length()-1;
    chipText=new SpannableString(displayText);
    if(!mNoChips)
      try
        {
        final DrawableRecipientChip chip=constructChipSpan(entry,pressed,false /*
                                                                                * leave space for contact
                                                                                * icon
                                                                                */);
        chipText.setSpan(chip,0,textLength,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        chip.setOriginalText(chipText.toString());
        }
      catch(final NullPointerException e)
        {
        Log.e(TAG,e.getMessage(),e);
        return null;
        }
    return chipText;
    }

  private void submitItemAtPosition(final int position)
    {
    final RecipientEntry entry=createValidatedEntry(getAdapter().getItem(position));
    if(entry==null)
      return;
    clearComposingText();
    final int end=getSelectionEnd();
    final int start=mTokenizer.findTokenStart(getText(),end);
    final Editable editable=getText();
    QwertyKeyListener.markAsReplaced(editable,start,end,"");
    final CharSequence chip=createChip(entry,false);
    if(chip!=null&&start>=0&&end>=0)
      editable.replace(start,end,chip);
    sanitizeBetween();
    if(mChipListener!=null)
      mChipListener.onDataChanged();
    }

  private RecipientEntry createValidatedEntry(final 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.
    final 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;
    }

  /** Returns a collection of contact Id for each chip inside this View. */
  /* package */Collection<Long> getContactIds()
    {
    final Set<Long> result=new HashSet<Long>();
    final DrawableRecipientChip[] chips=getSortedRecipients();
    if(chips!=null)
      for(final DrawableRecipientChip chip : chips)
        result.add(chip.getContactId());
    return result;
    }

  /** Returns a collection of data Id for each chip inside this View. May be null. */
  /* package */Collection<Long> getDataIds()
    {
    final Set<Long> result=new HashSet<Long>();
    final DrawableRecipientChip[] chips=getSortedRecipients();
    if(chips!=null)
      for(final DrawableRecipientChip chip : chips)
        result.add(chip.getDataId());
    return result;
    }

  // Visible for testing.
  /* package */DrawableRecipientChip[] getSortedRecipients()
    {
    final DrawableRecipientChip[] recips=getSpannable().getSpans(0,getText().length(),DrawableRecipientChip.class);
    final ArrayList<DrawableRecipientChip> recipientsList=new ArrayList<DrawableRecipientChip>(Arrays.asList(recips));
    final Spannable spannable=getSpannable();
    Collections.sort(recipientsList,new Comparator<DrawableRecipientChip>()
      {
        @Override
        public int compare(final DrawableRecipientChip first,final DrawableRecipientChip second)
          {
          final int firstStart=spannable.getSpanStart(first);
          final 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()]);
    }

  @Override
  public boolean onActionItemClicked(final ActionMode mode,final MenuItem item)
    {
    return false;
    }

  @Override
  public void onDestroyActionMode(final ActionMode mode)
    {}

  @Override
  public boolean onPrepareActionMode(final ActionMode mode,final Menu menu)
    {
    return false;
    }

  /**
   * No chips are selectable.
   */
  @Override
  public boolean onCreateActionMode(final ActionMode mode,final Menu menu)
    {
    return false;
    }

  // Visible for testing.
  /* package */ImageSpan getMoreChip()
    {
    final MoreImageSpan[] moreSpans=getSpannable().getSpans(0,getText().length(),MoreImageSpan.class);
    return moreSpans!=null&&moreSpans.length>0 ? moreSpans[0] : null;
    }

  private MoreImageSpan createMoreSpan(final int count)
    {
    final String moreText=String.format(mMoreItem.getText().toString(),count);
    final TextPaint morePaint=new TextPaint(getPaint());
    morePaint.setTextSize(mMoreItem.getTextSize());
    morePaint.setColor(mMoreItem.getCurrentTextColor());
    final int width=(int)morePaint.measureText(moreText)+mMoreItem.getPaddingLeft()+mMoreItem.getPaddingRight();
    final int height=getLineHeight();
    final Bitmap drawable=Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
    final Canvas canvas=new Canvas(drawable);
    int adjustedHeight=height;
    final Layout layout=getLayout();
    if(layout!=null)
      adjustedHeight-=layout.getLineDescent(0);
    canvas.drawText(moreText,0,moreText.length(),0,adjustedHeight,morePaint);
    final Drawable result=new BitmapDrawable(getResources(),drawable);
    result.setBounds(0,0,width,height);
    return new MoreImageSpan(result);
    }

  // Visible for testing.
  /* package */void createMoreChipPlainText()
    {
    // Take the first <= CHIP_LIMIT addresses and get to the end of the second one.
    final 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;
    final int tokenCount=countTokens(text);
    final MoreImageSpan moreSpan=createMoreSpan(tokenCount-CHIP_LIMIT);
    final 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;
    }

  // Visible for testing.
  /* package */int countTokens(final 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;
    }

  /**
   * 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.
   */
  // Visible for testing.
  /* package */void createMoreChip()
    {
    if(mNoChips)
      {
      createMoreChipPlainText();
      return;
      }
    if(!mShouldShrink)
      return;
    final ImageSpan[] tempMore=getSpannable().getSpans(0,getText().length(),MoreImageSpan.class);
    if(tempMore.length>0)
      getSpannable().removeSpan(tempMore[0]);
    final DrawableRecipientChip[] recipients=getSortedRecipients();
    if(recipients==null||recipients.length<=CHIP_LIMIT)
      {
      mMoreChip=null;
      return;
      }
    final Spannable spannable=getSpannable();
    final int numRecipients=recipients.length;
    final int overage=numRecipients-CHIP_LIMIT;
    final MoreImageSpan moreSpan=createMoreSpan(overage);
    mRemovedSpans=new ArrayList<DrawableRecipientChip>();
    int totalReplaceStart=0;
    int totalReplaceEnd=0;
    final 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]))
        {
        final int spanStart=spannable.getSpanStart(recipients[i]);
        final 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();
    final int end=Math.max(totalReplaceStart,totalReplaceEnd);
    final int start=Math.min(totalReplaceStart,totalReplaceEnd);
    final 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());
    }

  /**
   * Replace the more chip, if it exists, with all of the recipient chips it had replaced when the
   * RecipientEditTextView gains focus.
   */
  // Visible for testing.
  /* package */void removeMoreChip()
    {
    if(mMoreChip!=null)
      {
      final 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.
        final 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]);
        final Editable editable=getText();
        for(final 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,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          }
        mRemovedSpans.clear();
        }
      }
    }

  /**
   * 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.
   */
  private DrawableRecipientChip selectChip(final DrawableRecipientChip currentChip)
    {
    if(shouldShowEditableText(currentChip))
      {
      final CharSequence text=currentChip.getValue();
      final Editable editable=getText();
      final Spannable spannable=getSpannable();
      final int spanStart=spannable.getSpanStart(currentChip);
      final 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,false);
      }
    else if(currentChip.getContactId()==RecipientEntry.GENERATED_CONTACT||currentChip.isGalContact())
      {
      final int start=getChipStart(currentChip);
      final int end=getChipEnd(currentChip);
      getSpannable().removeSpan(currentChip);
      DrawableRecipientChip newChip;
      try
        {
        if(mNoChips)
          return null;
        newChip=constructChipSpan(currentChip.getEntry(),true,false);
        }
      catch(final NullPointerException e)
        {
        Log.e(TAG,e.getMessage(),e);
        return null;
        }
      final 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)));
      showAddress(newChip,mAddressPopup,getWidth());
      setCursorVisible(false);
      return newChip;
      }
    else
      {
      final int start=getChipStart(currentChip);
      final int end=getChipEnd(currentChip);
      getSpannable().removeSpan(currentChip);
      DrawableRecipientChip newChip;
      try
        {
        newChip=constructChipSpan(currentChip.getEntry(),true,false);
        }
      catch(final NullPointerException e)
        {
        Log.e(TAG,e.getMessage(),e);
        return null;
        }
      final 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)));
      showAlternates(newChip,mAlternatesPopup,getWidth());
      setCursorVisible(false);
      return newChip;
      }
    }

  private boolean shouldShowEditableText(final DrawableRecipientChip currentChip)
    {
    final long contactId=currentChip.getContactId();
    return contactId==RecipientEntry.INVALID_CONTACT||!isPhoneQuery()&&contactId==RecipientEntry.GENERATED_CONTACT;
    }

  private void showAddress(final DrawableRecipientChip currentChip,final ListPopupWindow popup,final int width)
    {
    if(!mAttachedToWindow)
      return;
    final int line=getLayout().getLineForOffset(getChipStart(currentChip));
    final int bottom=calculateOffsetFromBottom(line);
    // Align the alternates popup with the left side of the View,
    // regardless of the position of the chip tapped.
    popup.setWidth(width);
    popup.setAnchorView(this);
    popup.setVerticalOffset(bottom);
    popup.setAdapter(createSingleAddressAdapter(currentChip));
    popup.setOnItemClickListener(new OnItemClickListener()
      {
        @Override
        public void onItemClick(final AdapterView<?> parent,final View view,final int position,final long id)
          {
          unselectChip(currentChip);
          popup.dismiss();
          }
      });
    popup.show();
    final ListView listView=popup.getListView();
    listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
    listView.setItemChecked(0,true);
    }

  /**
   * 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.
   */
  private void unselectChip(final DrawableRecipientChip chip)
    {
    final int start=getChipStart(chip);
    final int end=getChipEnd(chip);
    final 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,false),start,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
      catch(final NullPointerException e)
        {
        Log.e(TAG,e.getMessage(),e);
        }
      }
    setCursorVisible(true);
    setSelection(editable.length());
    if(mAlternatesPopup!=null&&mAlternatesPopup.isShowing())
      mAlternatesPopup.dismiss();
    }

  /**
   * Return whether a touch event was inside the delete target of a selected chip. It is in the delete target if: 1)
   * the x and y points of the event are within the delete assset. 2) the point tapped would have caused a cursor to
   * appear right after the selected chip.
   *
   * @return boolean
   */
  private boolean isInDelete(final DrawableRecipientChip chip,final int offset,final float x,final float y)
    {
    // Figure out the bounds of this chip and whether or not
    // the user clicked in the X portion.
    // TODO: Should x and y be used, or removed?
    return chip.isSelected()&&offset==getChipEnd(chip);
    }

  /**
   * Remove the chip and any text associated with it from the RecipientEditTextView.
   *
   * @param alsoNotifyAboutDataChanges
   */
  // Visible for testing.
  /* package */void removeChip(final DrawableRecipientChip chip,final boolean alsoNotifyAboutDataChanges)
    {
    if(!alsoNotifyAboutDataChanges)
      --mPreviousChipsCount;
    final Spannable spannable=getSpannable();
    final int spanStart=spannable.getSpanStart(chip);
    final int spanEnd=spannable.getSpanEnd(chip);
    final Editable text=getText();
    int toDelete=spanEnd;
    final 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();
    }

  /**
   * Replace this currently selected chip with a new chip that uses the contact data provided.
   */
  // Visible for testing.
  /* package */void replaceChip(final DrawableRecipientChip chip,final RecipientEntry entry)
    {
    final boolean wasSelected=chip==mSelectedChip;
    if(wasSelected)
      mSelectedChip=null;
    final int start=getChipStart(chip);
    final int end=getChipEnd(chip);
    getSpannable().removeSpan(chip);
    final Editable editable=getText();
    final 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();
    if(mChipListener!=null)
      mChipListener.onDataChanged();
    }

  /**
   * 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.
   */
  public void onClick(final DrawableRecipientChip chip,final int offset,final float x,final float y)
    {
    if(chip.isSelected())
      if(isInDelete(chip,offset,x,y))
        removeChip(chip,true);
      else clearSelectedChip();
    }

  private boolean chipsPending()
    {
    return mPendingChipsCount>0||mRemovedSpans!=null&&mRemovedSpans.size()>0;
    }

  @Override
  public void removeTextChangedListener(final TextWatcher watcher)
    {
    mTextWatcher=null;
    super.removeTextChangedListener(watcher);
    }

  public boolean lastCharacterIsCommitCharacter(final CharSequence s)
    {
    char last;
    final int end=getSelectionEnd()==0 ? 0 : getSelectionEnd()-1;
    final 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;
    }

  public boolean isGeneratedContact(final DrawableRecipientChip chip)
    {
    final long contactId=chip.getContactId();
    return contactId==RecipientEntry.INVALID_CONTACT||!isPhoneQuery()&&contactId==RecipientEntry.GENERATED_CONTACT;
    }

  /**
   * Handles pasting a {@link ClipData} to this {@link RecipientEditTextView}.
   */
  private void handlePasteClip(final ClipData clip)
    {
    removeTextChangedListener(mTextWatcher);
    if(clip!=null&&clip.getDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN))
      for(int i=0;i<clip.getItemCount();i++)
        {
        final CharSequence paste=clip.getItemAt(i).getText();
        if(paste!=null)
          {
          final int start=getSelectionStart();
          final int end=getSelectionEnd();
          final Editable editable=getText();
          if(start>=0&&end>=0&&start!=end)
            editable.append(paste,start,end);
          else editable.insert(end,paste);
          handlePasteAndReplace();
          }
        }
    mHandler.post(mAddTextWatcher);
    }

  @Override
  public boolean onTextContextMenuItem(final int id)
    {
    if(id==android.R.id.paste)
      {
      final ClipboardManager clipboard=(ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE);
      handlePasteClip(clipboard.getPrimaryClip());
      return true;
      }
    return super.onTextContextMenuItem(id);
    }

  private void handlePasteAndReplace()
    {
    final ArrayList<DrawableRecipientChip> created=handlePaste();
    if(created!=null&&created.size()>0)
      {
      // Perform reverse lookups on the pasted contacts.
      final IndividualReplacementTask replace=new IndividualReplacementTask();
      replace.execute(created);
      }
    }

  // Visible for testing.
  /* package */ArrayList<DrawableRecipientChip> handlePaste()
    {
    final String text=getText().toString();
    final int originalTokenStart=mTokenizer.findTokenStart(text,getSelectionEnd());
    final String lastAddress=text.substring(originalTokenStart);
    int tokenStart=originalTokenStart;
    int prevTokenStart=0;
    DrawableRecipientChip findChip=null;
    final 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))
      {
      final Editable editable=getText();
      tokenStart=editable.toString().indexOf(lastAddress,originalTokenStart);
      commitChip(tokenStart,editable.length(),editable);
      created.add(findChip(tokenStart));
      }
    return created;
    }

  // Visible for testing.
  /* package */int movePastTerminators(int tokenEnd)
    {
    if(tokenEnd>=length())
      return tokenEnd;
    final 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;
    }

  @Override
  public boolean onDown(final MotionEvent e)
    {
    return false;
    }

  @Override
  public boolean onFling(final MotionEvent e1,final MotionEvent e2,final float velocityX,final float velocityY)
    {
    // Do nothing.
    return false;
    }

  @Override
  public void onLongPress(final MotionEvent event)
    {
    if(mSelectedChip!=null)
      return;
    final float x=event.getX();
    final float y=event.getY();
    final int offset=putOffsetInRange(x,y);
    final DrawableRecipientChip currentChip=findChip(offset);
    if(currentChip!=null)
      // Copy the selected chip email address.
      showCopyDialog(currentChip.getEntry().getDestination());
    }

  // The following methods are used to provide some functionality on older versions of Android
  // These methods were copied out of JB MR2's TextView
  // ///////////////////////////////////////////////
  private int supportGetOffsetForPosition(final float x,final float y)
    {
    if(getLayout()==null)
      return -1;
    final int line=supportGetLineAtCoordinate(y);
    final int offset=supportGetOffsetAtCoordinate(line,x);
    return offset;
    }

  private float supportConvertToLocalHorizontalCoordinate(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 int supportGetLineAtCoordinate(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 int supportGetOffsetAtCoordinate(final int line,float x)
    {
    x=supportConvertToLocalHorizontalCoordinate(x);
    return getLayout().getOffsetForHorizontal(line,x);
    }

  // ///////////////////////////////////////////////
  private void showCopyDialog(final String address)
    {
    if(!mAttachedToWindow)
      return;
    mCopyAddress=address;
    mCopyDialog.setTitle(address);
    mCopyDialog.setContentView(R.layout.copy_chip_dialog_layout);
    mCopyDialog.setCancelable(true);
    mCopyDialog.setCanceledOnTouchOutside(true);
    final 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;
    final String buttonTitle=getContext().getResources().getString(btnTitleId);
    button.setText(buttonTitle);
    mCopyDialog.setOnDismissListener(this);
    mCopyDialog.show();
    }

  @Override
  public boolean onScroll(final MotionEvent e1,final MotionEvent e2,final float distanceX,final float distanceY)
    {
    // Do nothing.
    return false;
    }

  @Override
  public void onShowPress(final MotionEvent e)
    {
    // Do nothing.
    }

  @Override
  public boolean onSingleTapUp(final MotionEvent e)
    {
    // Do nothing.
    return false;
    }

  @Override
  public void onDismiss(final DialogInterface dialog)
    {
    mCopyAddress=null;
    }

  @Override
  public void onClick(final View v)
    {
    // Copy this to the clipboard.
    final ClipboardManager clipboard=(ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE);
    clipboard.setPrimaryClip(ClipData.newPlainText("",mCopyAddress));
    mCopyDialog.dismiss();
    }

  protected boolean isPhoneQuery()
    {
    return getAdapter()!=null&&getAdapter().getQueryType()==BaseRecipientAdapter.QUERY_TYPE_PHONE;
    }

  @Override
  public BaseRecipientAdapter getAdapter()
    {
    return (BaseRecipientAdapter)super.getAdapter();
    }

  // /////////////////////////////////////////////////////////////////////
  // MoreImageSpan //
  // ////////////////
  /** MoreImageSpan is a simple class created for tracking the existence of a more chip across activity restarts */
  private class MoreImageSpan extends ImageSpan
    {
    public MoreImageSpan(final Drawable b)
      {
      super(b);
      }
    }

  // //////////////////////////////////////////////////////////////////
  // RecipientTextWatcher //
  // ///////////////////////
  private class RecipientTextWatcher implements TextWatcher
    {
    @Override
    public void afterTextChanged(final Editable s)
      {
      // If the text has been set to null or empty, make sure we remove
      // all the spans we applied.
      if(TextUtils.isEmpty(s))
        {
        // Remove all the chips spans.
        final Spannable spannable=getSpannable();
        final DrawableRecipientChip[] chips=spannable.getSpans(0,getText().length(),DrawableRecipientChip.class);
        for(final DrawableRecipientChip chip : chips)
          spannable.removeSpan(chip);
        if(mMoreChip!=null)
          spannable.removeSpan(mMoreChip);
        return;
        }
      // Get whether there are any recipients pending addition to the
      // view. If there are, don't do anything in the text watcher.
      if(chipsPending())
        return;
      // If the user is editing a chip, don't clear it.
      if(mSelectedChip!=null)
        if(!isGeneratedContact(mSelectedChip))
          {
          setCursorVisible(true);
          setSelection(getText().length());
          clearSelectedChip();
          }
        else return;
      final int length=s.length();
      // Make sure there is content there to parse and that it is
      // not just the commit character.
      if(length>1)
        {
        if(lastCharacterIsCommitCharacter(s))
          {
          commitByCharacter();
          return;
          }
        char last;
        final int end=getSelectionEnd()==0 ? 0 : getSelectionEnd()-1;
        final int len=length()-1;
        if(end!=len)
          last=s.charAt(end);
        else last=s.charAt(len);
        if(last==COMMIT_CHAR_SPACE)
          if(!isPhoneQuery())
            {
            // Check if this is a valid email address. If it is,
            // commit it.
            final String text=getText().toString();
            final int tokenStart=mTokenizer.findTokenStart(text,getSelectionEnd());
            final String sub=text.substring(tokenStart,mTokenizer.findTokenEnd(text,tokenStart));
            if(!TextUtils.isEmpty(sub)&&mValidator!=null&&mValidator.isValid(sub))
              commitByCharacter();
            }
        }
      }

    @Override
    public void onTextChanged(final CharSequence s,final int start,final int before,final int count)
      {
      // The user deleted some text OR some text was replaced; check to
      // see if the insertion point is on a space
      // following a chip.
      if(count!=before)
        {
        final DrawableRecipientChip[] chips=getSpannable().getSpans(0,getText().length(),DrawableRecipientChip.class);
        final int chipsCount=chips.length;
        if(mPreviousChipsCount>chipsCount&&mChipListener!=null)
          mChipListener.onDataChanged();
        mPreviousChipsCount=chipsCount;
        }
      if(before-count==1)
        {
        // If the item deleted is a space, and the thing before the
        // space is a chip, delete the entire span.
        final int selStart=getSelectionStart();
        final DrawableRecipientChip[] repl=getSpannable().getSpans(selStart,selStart,DrawableRecipientChip.class);
        if(repl.length>0)
          {
          // There is a chip there! Just remove it.
          final Editable editable=getText();
          // Add the separator token.
          final int tokenStart=mTokenizer.findTokenStart(editable,selStart);
          int tokenEnd=mTokenizer.findTokenEnd(editable,tokenStart);
          tokenEnd=tokenEnd+1;
          if(tokenEnd>editable.length())
            tokenEnd=editable.length();
          editable.delete(tokenStart,tokenEnd);
          getSpannable().removeSpan(repl[0]);
          }
        }
      else if(count>before)
        if(mSelectedChip!=null&&isGeneratedContact(mSelectedChip))
          if(lastCharacterIsCommitCharacter(s))
            {
            commitByCharacter();
            return;
            }
      }

    @Override
    public void beforeTextChanged(final CharSequence s,final int start,final int count,final int after)
      {
      // Do nothing.
      // final DrawableRecipientChip[] chips = getSpannable().getSpans(0, getText().length(),
      // DrawableRecipientChip.class);
      // previousChipsCount = chips.length;
      }
    }

  // ////////////////////////////////////////////////////////////
  // RecipientReplacementTask //
  // ///////////////////////////
  private class RecipientReplacementTask extends AsyncTask<Void,Void,Void>
    {
    private DrawableRecipientChip createFreeChip(final RecipientEntry entry)
      {
      try
        {
        if(mNoChips)
          return null;
        return constructChipSpan(entry,false,false /* leave space for contact icon */);
        }
      catch(final NullPointerException e)
        {
        Log.e(TAG,e.getMessage(),e);
        return null;
        }
      }

    @Override
    protected void onPreExecute()
      {
      // Ensure everything is in chip-form already, so we don't have text that slowly gets
      // replaced
      final List<DrawableRecipientChip> originalRecipients=new ArrayList<DrawableRecipientChip>();
      final DrawableRecipientChip[] existingChips=getSortedRecipients();
      for(int i=0;i<existingChips.length;i++)
        originalRecipients.add(existingChips[i]);
      if(mRemovedSpans!=null)
        originalRecipients.addAll(mRemovedSpans);
      final List<DrawableRecipientChip> replacements=new ArrayList<DrawableRecipientChip>(originalRecipients.size());
      for(final DrawableRecipientChip chip : originalRecipients)
        if(RecipientEntry.isCreatedRecipient(chip.getEntry().getContactId())&&getSpannable().getSpanStart(chip)!=-1)
          replacements.add(createFreeChip(chip.getEntry()));
        else replacements.add(null);
      processReplacements(originalRecipients,replacements);
      }

    @Override
    protected Void doInBackground(final Void... params)
      {
      if(mIndividualReplacements!=null)
        mIndividualReplacements.cancel(true);
      // For each chip in the list, look up the matching contact.
      // If there is a match, replace that chip with the matching
      // chip.
      final ArrayList<DrawableRecipientChip> recipients=new ArrayList<DrawableRecipientChip>();
      final DrawableRecipientChip[] existingChips=getSortedRecipients();
      for(int i=0;i<existingChips.length;i++)
        recipients.add(existingChips[i]);
      if(mRemovedSpans!=null)
        recipients.addAll(mRemovedSpans);
      final ArrayList<String> addresses=new ArrayList<String>();
      DrawableRecipientChip chip;
      for(int i=0;i<recipients.size();i++)
        {
        chip=recipients.get(i);
        if(chip!=null)
          addresses.add(createAddressText(chip.getEntry()));
        }
      final BaseRecipientAdapter adapter=getAdapter();
      RecipientAlternatesAdapter.getMatchingRecipients(getContext(),adapter,addresses,adapter.getAccount(),new RecipientMatchCallback()
        {
          @Override
          public void matchesFound(final Map<String,RecipientEntry> entries)
            {
            final ArrayList<DrawableRecipientChip> replacements=new ArrayList<DrawableRecipientChip>();
            for(final DrawableRecipientChip temp : recipients)
              {
              RecipientEntry entry=null;
              if(temp!=null&&RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())&&getSpannable().getSpanStart(temp)!=-1)
                // Replace this.
                entry=createValidatedEntry(entries.get(tokenizeAddress(temp.getEntry().getDestination())));
              if(entry!=null)
                replacements.add(createFreeChip(entry));
              else replacements.add(null);
              }
            processReplacements(recipients,replacements);
            }

          @Override
          public void matchesNotFound(final Set<String> unfoundAddresses)
            {
            final List<DrawableRecipientChip> replacements=new ArrayList<DrawableRecipientChip>(unfoundAddresses.size());
            for(final DrawableRecipientChip temp : recipients)
              if(temp!=null&&RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())&&getSpannable().getSpanStart(temp)!=-1)
                {
                if(unfoundAddresses.contains(temp.getEntry().getDestination()))
                  replacements.add(createFreeChip(temp.getEntry()));
                else replacements.add(null);
                }
              else replacements.add(null);
            processReplacements(recipients,replacements);
            }
        });
      return null;
      }

    private void processReplacements(final List<DrawableRecipientChip> recipients,final List<DrawableRecipientChip> replacements)
      {
      if(replacements!=null&&replacements.size()>0)
        {
        final Runnable runnable=new Runnable()
          {
            @Override
            public void run()
              {
              final Editable text=new SpannableStringBuilder(getText());
              int i=0;
              for(final DrawableRecipientChip chip : recipients)
                {
                final DrawableRecipientChip replacement=replacements.get(i);
                if(replacement!=null)
                  {
                  final RecipientEntry oldEntry=chip.getEntry();
                  final RecipientEntry newEntry=replacement.getEntry();
                  final boolean isBetter=RecipientAlternatesAdapter.getBetterRecipient(oldEntry,newEntry)==newEntry;
                  if(isBetter)
                    {
                    // Find the location of the chip in the text currently shown.
                    final int start=text.getSpanStart(chip);
                    if(start!=-1)
                      {
                      // Replacing the entirety of what the chip represented,
                      // including the extra space dividing it from other chips.
                      final int end=Math.min(text.getSpanEnd(chip)+1,text.length());
                      text.removeSpan(chip);
                      // Make sure we always have just 1 space at the end to
                      // separate this chip from the next chip.
                      final SpannableString displayText=new SpannableString(createAddressText(replacement.getEntry()).trim()+" ");
                      displayText.setSpan(replacement,0,displayText.length()-1,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                      // Replace the old text we found with with the new display
                      // text, which now may also contain the display name of the
                      // recipient.
                      text.replace(start,end,displayText);
                      replacement.setOriginalText(displayText.toString());
                      replacements.set(i,null);
                      recipients.set(i,replacement);
                      }
                    }
                  }
                i++;
                }
              setText(text);
              }
          };
        if(Looper.myLooper()==Looper.getMainLooper())
          runnable.run();
        else mHandler.post(runnable);
        }
      }
    }

  // ////////////////////////////////////////////////////////////////////////
  // IndividualReplacementTask //
  // ////////////////////////////
  private class IndividualReplacementTask extends AsyncTask<ArrayList<DrawableRecipientChip>,Void,Void>
    {
    @Override
    protected Void doInBackground(final ArrayList<DrawableRecipientChip>... params)
      {
      // For each chip in the list, look up the matching contact.
      // If there is a match, replace that chip with the matching
      // chip.
      final ArrayList<DrawableRecipientChip> originalRecipients=params[0];
      final ArrayList<String> addresses=new ArrayList<String>();
      DrawableRecipientChip chip;
      for(int i=0;i<originalRecipients.size();i++)
        {
        chip=originalRecipients.get(i);
        if(chip!=null)
          addresses.add(createAddressText(chip.getEntry()));
        }
      final BaseRecipientAdapter adapter=getAdapter();
      RecipientAlternatesAdapter.getMatchingRecipients(getContext(),adapter,addresses,adapter.getAccount(),new RecipientMatchCallback()
        {
          @Override
          public void matchesFound(final Map<String,RecipientEntry> entries)
            {
            for(final DrawableRecipientChip temp : originalRecipients)
              if(RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())&&getSpannable().getSpanStart(temp)!=-1)
                {
                // Replace this.
                final RecipientEntry entry=createValidatedEntry(entries.get(tokenizeAddress(temp.getEntry().getDestination()).toLowerCase(Locale.getDefault())));
                if(entry!=null)
                  mHandler.post(new Runnable()
                    {
                      @Override
                      public void run()
                        {
                        replaceChip(temp,entry);
                        }
                    });
                }
            }

          @Override
          public void matchesNotFound(final Set<String> unfoundAddresses)
            {
            // No action required
            }
        });
      return null;
      }
    }

  // ////////////////////////////////////////////////added functionality///////////////////////////////////////
  public void setChipListener(final IChipListener chipListener)
    {
    mChipListener=chipListener;
    }

  /** adds a recipient to the view. note that it should be called when the view has determined its size */
  public void addRecipient(final RecipientEntry entry,final boolean alsoNotifyAboutDataChanges)
    {
    if(entry==null)
      return;
    // clearComposingText();
    final Editable editable=getText();
    // QwertyKeyListener.markAsReplaced(editable, start, end, "");
    final CharSequence chip=createChip(entry,false);
    if(!alsoNotifyAboutDataChanges)
      ++mPreviousChipsCount;
    // expand();
    if(chip!=null)
      editable.append(chip);
    sanitizeBetween();
    // shrink();
    }

  /** removes a chip of a recipient from the view */
  public void removeRecipient(final RecipientEntry entry,final boolean alsoNotifyAboutDataChanges)
    {
    if(entry==null)
      return;
    // expand();
    final DrawableRecipientChip[] chips=getSpannable().getSpans(0,getText().length(),DrawableRecipientChip.class);
    final List<DrawableRecipientChip> chipsToRemove=new ArrayList<DrawableRecipientChip>();
    for(final DrawableRecipientChip chip : chips)
      if(chip.getDataId()==entry.getDataId())
        chipsToRemove.add(chip);
    for(final DrawableRecipientChip chip : chipsToRemove)
      removeChip(chip,alsoNotifyAboutDataChanges);
    // shrink();
    }

  public void removeAllRecipients(final boolean alsoNotifyAboutDataChanges)
    {
    final DrawableRecipientChip[] chips=getSpannable().getSpans(0,getText().length(),DrawableRecipientChip.class);
    for(final DrawableRecipientChip chip : chips)
      removeChip(chip,alsoNotifyAboutDataChanges);
    }

  /** returns a collection of all of the chips' items. key is the contact id, and the value is the recipient itself */
  public Set<RecipientEntry> getChosenRecipients()
    {
    final Set<RecipientEntry> result=new HashSet<RecipientEntry>();
    final DrawableRecipientChip[] chips=getSpannable().getSpans(0,getText().length(),DrawableRecipientChip.class);
    for(final DrawableRecipientChip chip : chips)
      {
      final RecipientEntry recipientEntry=chip.getEntry();
      result.add(recipientEntry);
      }
    return result;
    }

  /**
   * sets exact recipients to be chosen. <br/>
   * each previous chip will stay only if it really matches one of the recipients. <br/>
   * note that this function will not notify about data changes
   */
  public void setChosenRecipients(final Set<RecipientEntry> recipientsToSet)
    {
    final Set<RecipientEntry> currentRecipients=getChosenRecipients();
    final List<RecipientEntry> toRemove=new ArrayList<RecipientEntry>(),toAdd=new ArrayList<RecipientEntry>();
    // check which to remove - items that exist on the editText but not on the list
    for(final RecipientEntry recipientEntry : currentRecipients)
      if(!recipientsToSet.contains(recipientEntry))
        toRemove.add(recipientEntry);
    // check which to add - items that exist on the list, but not on the editText
    for(final RecipientEntry recipientEntry : recipientsToSet)
      if(!currentRecipients.contains(recipientEntry))
        toAdd.add(recipientEntry);
    for(final RecipientEntry recipientEntry : toRemove)
      removeRecipient(recipientEntry,false);
    for(final RecipientEntry recipientEntry : toAdd)
      addRecipient(recipientEntry,false);
    }

  public void setFocusBehavior(final EnumSet<FocusBehavior> focusBehavior)
    {
    mFocusBehavior.clear();
    mFocusBehavior.addAll(focusBehavior);
    }
  }