content: /*
  
   * 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 android.annotation.TargetApi;
  import android.app.Activity;
  import android.app.AlertDialog;
  import android.app.DialogFragment;
  import android.content.ClipData;
  import android.content.ClipDescription;
  import android.content.ClipboardManager;
  import android.content.Context;
  import android.content.DialogInterface;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.graphics.Bitmap;
  import android.graphics.BitmapFactory;
  import android.graphics.BitmapShader;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Matrix;
  import android.graphics.Paint;
  import android.graphics.Paint.Style;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.graphics.Shader.TileMode;
  import android.graphics.drawable.BitmapDrawable;
  import android.graphics.drawable.Drawable;
  import android.graphics.drawable.StateListDrawable;
  import android.os.AsyncTask;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Parcelable;
  import androidx.annotation.NonNull;
  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.util.Rfc822Token;
  import android.text.util.Rfc822Tokenizer;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.ActionMode;
  import android.view.ActionMode.Callback;
  import android.view.DragEvent;
  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.ViewParent;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityManager;
  import android.view.inputmethod.EditorInfo;
  import android.view.inputmethod.InputConnection;
  import android.widget.AdapterView;
  import android.widget.AdapterView.OnItemClickListener;
  import android.widget.Filterable;
  import android.widget.ListAdapter;
  import android.widget.ListPopupWindow;
  import android.widget.ListView;
  import android.widget.MultiAutoCompleteTextView;
  import android.widget.PopupWindow;
  import android.widget.ScrollView;
  import android.widget.TextView;
  
  import com.android.ex.chips.DropdownChipLayouter.PermissionRequestDismissedListener;
  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.ReplacementDrawableSpan;
  import com.android.ex.chips.recipientchip.VisibleRecipientChip;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * 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
          OnItemClickListener, Callback, RecipientAlternatesAdapter.OnCheckedItemChangedListener,
          GestureDetector.OnGestureListener, TextView.OnEditorActionListener,
          DropdownChipLayouter.ChipDeleteListener, PermissionRequestDismissedListener {
      private static final String TAG = "RecipientEditTextView";
  
      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 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;
      public static final String STATE_TEXT_VIEW = "savedTextView";
      public static final String STATE_CURRENT_WARNING_TEXT = "savedCurrentWarningText";
  
      private int mUnselectedChipTextColor;
      private int mUnselectedChipBackgroundColor;
  
      // Work variables to avoid re-allocation on every typed character.
      private final Rect mRect = new Rect();
      private final int[] mCoords = new int[2];
  
      // Resources for displaying chips.
      private Drawable mChipBackground = null;
      private Drawable mChipDelete = null;
      private Drawable mInvalidChipBackground;
  
      // Possible attr overrides
      private float mChipHeight;
      private float mChipFontSize;
      private float mLineSpacingExtra;
      private int mChipTextStartPadding;
      private int mChipTextEndPadding;
      private final int mTextHeight;
      private boolean mDisableDelete;
      private int mMaxLines;
      private int mWarningIconHeight;
  
      /**
       * Enumerator for avatar position. See attr.xml for more details.
       * 0 for end, 1 for start.
       */
      private int mAvatarPosition;
      private static final int AVATAR_POSITION_END = 0;
      private static final int AVATAR_POSITION_START = 1;
  
      private Paint mWorkPaint = new Paint();
  
      private Tokenizer mTokenizer;
      private Validator mValidator;
      private Handler mHandler;
      private TextWatcher mTextWatcher;
      protected DropdownChipLayouter mDropdownChipLayouter;
  
      private View mDropdownAnchor = this;
      private ListPopupWindow mAlternatesPopup;
      private ListPopupWindow mAddressPopup;
      private View mAlternatePopupAnchor;
      private OnItemClickListener mAlternatesListener;
  
      private DrawableRecipientChip mSelectedChip;
      private Bitmap mDefaultContactPhoto;
      private Bitmap mWarningIcon;
      private ReplacementDrawableSpan mMoreChip;
      private TextView mMoreItem;
  
      private int mCurrentSuggestionCount;
  
      // VisibleForTesting
      final ArrayList<String> mPendingChips = new ArrayList<String>();
  
      private int mPendingChipsCount = 0;
      private int mCheckedItem;
      private boolean mNoChipMode = false;
      private boolean mShouldShrink = true;
      private boolean mRequiresShrinkWhenNotGone = false;
  
      // VisibleForTesting
      ArrayList<DrawableRecipientChip> mTemporaryRecipients;
  
      private ArrayList<DrawableRecipientChip> mHiddenSpans;
  
      // Chip copy fields.
      private GestureDetector mGestureDetector;
  
      // 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 boolean mDragEnabled = false;
  
      private boolean mAttachedToWindow;
  
      private final Runnable mAddTextWatcher = new Runnable() {
          @Override
          public void run() {
              if (mTextWatcher == null) {
                  mTextWatcher = new RecipientTextWatcher();
                  addTextChangedListener(mTextWatcher);
              }
          }
      };
  
      private IndividualReplacementTask mIndividualReplacements;
  
      private Runnable mHandlePendingChips = new Runnable() {
  
          @Override
          public void run() {
              handlePendingChips();
          }
  
      };
  
      private Runnable mDelayedShrink = new Runnable() {
  
          @Override
          public void run() {
              shrink();
          }
  
      };
  
      private RecipientEntryItemClickedListener mRecipientEntryItemClickedListener;
  
      private RecipientChipAddedListener mRecipientChipAddedListener;
      private RecipientChipDeletedListener mRecipientChipDeletedListener;
  
      // A set of recipient addresses that are untrusted because they are outside of the user's
      // domain. We will show a warning for these addresses in the recipient chips.
      private Set<String> mUntrustedAddresses = new HashSet<>();
  
      private String mWarningTextTemplate = "";
      private String mWarningTitle = "";
      // Text of the warning dialog currently being displayed. Empty if no dialog currently displayed.
      private String mCurrentWarningText = "";
  
      /**
       * Sets this recipient edit text view to display warning icons in chips for the given addresses.
       *
       * @param untrustedAddresses The addresses to display warning icons for.
       * @param warningIcon The icon to show for each address.
       * @param warningIconHeight Height of the warning icon in
       * @param warningTextTemplate Text to display when warning icon is clicked.
       * @param warningTitle Title to display for text when warning icon is clicked.
       */
      public void setUntrustedAddressWarning(
              Set<String> untrustedAddresses,
              Bitmap warningIcon,
              int warningIconHeight,
              String warningTextTemplate,
              String warningTitle) {
          mUntrustedAddresses = untrustedAddresses;
          mWarningIcon = warningIcon;
          mWarningIconHeight = warningIconHeight;
          mWarningTextTemplate = warningTextTemplate;
          mWarningTitle = warningTitle;
      }
  
      public interface RecipientEntryItemClickedListener {
          /**
           * Callback that occurs whenever an auto-complete suggestion is clicked.
           * @param charactersTyped the number of characters typed by the user to provide the
           *                        auto-complete suggestions.
           * @param position the position in the dropdown list that the user clicked
           */
          void onRecipientEntryItemClicked(int charactersTyped, int position);
      }
  
      private PermissionsRequestItemClickedListener mPermissionsRequestItemClickedListener;
  
      /**
       * Listener for handling clicks on the {@link RecipientEntry} that have
       * {@link RecipientEntry#ENTRY_TYPE_PERMISSION_REQUEST} type.
       */
      public interface PermissionsRequestItemClickedListener {
  
          /**
           * Callback that occurs when user clicks the item that asks user to grant permissions to
           * the app.
           *
           * @param view View that asks for permission.
           */
          void onPermissionsRequestItemClicked(RecipientEditTextView view, String[] permissions);
  
          /**
           * Callback that occurs when user dismisses the item that asks user to grant permissions to
           * the app.
           */
          void onPermissionRequestDismissed();
      }
  
      /**
       * Listener for handling deletion of chips in the recipient edit text.
       */
      public interface RecipientChipDeletedListener {
          /**
           * Callback that occurs when a chip is deleted.
           * @param entry RecipientEntry that contains information about the chip.
           */
          void onRecipientChipDeleted(RecipientEntry entry);
      }
  
      /**
       * Listener for handling addition of chips in the recipient edit text.
       */
      public interface RecipientChipAddedListener {
          /**
           * Callback that occurs when a chip is added.
           *
           * @param entry RecipientEntry that contains information about the chip.
           */
          void onRecipientChipAdded(RecipientEntry entry);
      }
  
      public RecipientEditTextView(Context context, AttributeSet attrs) {
          super(context, attrs);
          setChipDimensions(context, attrs);
          mTextHeight = calculateTextHeight();
          mAlternatesPopup = new ListPopupWindow(context);
          setupPopupWindow(mAlternatesPopup);
          mAddressPopup = new ListPopupWindow(context);
          setupPopupWindow(mAddressPopup);
          mAlternatesListener = new OnItemClickListener() {
              @Override
              public void onItemClick(AdapterView<?> adapterView,View view, int position,
                      long rowId) {
                  mAlternatesPopup.setOnItemClickListener(null);
                  replaceChip(mSelectedChip, ((RecipientAlternatesAdapter) adapterView.getAdapter())
                          .getRecipientEntry(position));
                  Message delayed = Message.obtain(mHandler, DISMISS);
                  delayed.obj = mAlternatesPopup;
                  mHandler.sendMessageDelayed(delayed, DISMISS_DELAY);
                  clearComposingText();
              }
          };
          setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
          setOnItemClickListener(this);
          setCustomSelectionActionModeCallback(this);
          mHandler = new Handler() {
              @Override
              public void handleMessage(Message msg) {
                  if (msg.what == DISMISS) {
                      ((ListPopupWindow) msg.obj).dismiss();
                      return;
                  }
                  super.handleMessage(msg);
              }
          };
          mTextWatcher = new RecipientTextWatcher();
          addTextChangedListener(mTextWatcher);
          mGestureDetector = new GestureDetector(context, this);
          setOnEditorActionListener(this);
  
          setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context));
      }
  
      private void setupPopupWindow(ListPopupWindow popup) {
          popup.setOnDismissListener(new PopupWindow.OnDismissListener() {
              @Override
              public void onDismiss() {
                  clearSelectedChip();
              }
          });
      }
  
      private int calculateTextHeight() {
          final TextPaint paint = getPaint();
  
          mRect.setEmpty();
          // First measure the bounds of a sample text.
          final String textHeightSample = "a";
          paint.getTextBounds(textHeightSample, 0, textHeightSample.length(), mRect);
  
          mRect.left = 0;
          mRect.right = 0;
  
          return mRect.height();
      }
  
      public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
          mDropdownChipLayouter = dropdownChipLayouter;
          mDropdownChipLayouter.setDeleteListener(this);
          mDropdownChipLayouter.setPermissionRequestDismissedListener(this);
      }
  
      public void setRecipientEntryItemClickedListener(RecipientEntryItemClickedListener listener) {
          mRecipientEntryItemClickedListener = listener;
      }
  
      public void setPermissionsRequestItemClickedListener(
              PermissionsRequestItemClickedListener listener) {
          mPermissionsRequestItemClickedListener = listener;
      }
  
      public void setRecipientChipAddedListener(RecipientChipAddedListener listener) {
          mRecipientChipAddedListener = listener;
      }
  
      public void setRecipientChipDeletedListener(RecipientChipDeletedListener listener) {
          mRecipientChipDeletedListener = listener;
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          mAttachedToWindow = false;
      }
  
      @Override
      protected void onAttachedToWindow() {
          super.onAttachedToWindow();
          mAttachedToWindow = true;
  
          final int anchorId = getDropDownAnchor();
          if (anchorId != View.NO_ID) {
              mDropdownAnchor = getRootView().findViewById(anchorId);
          }
      }
  
      @Override
      public void setDropDownAnchor(int anchorId) {
          super.setDropDownAnchor(anchorId);
          if (anchorId != View.NO_ID) {
            mDropdownAnchor = getRootView().findViewById(anchorId);
          }
      }
  
      @Override
      public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
          if (action == EditorInfo.IME_ACTION_DONE) {
              if (commitDefault()) {
                  return true;
              }
              if (mSelectedChip != null) {
                  clearSelectedChip();
                  return true;
              } else if (hasFocus()) {
                  if (focusNext()) {
                      return true;
                  }
              }
          }
          return false;
      }
  
      @Override
      public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
          InputConnection connection = super.onCreateInputConnection(outAttrs);
          int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION;
          if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) {
              // clear the existing action
              outAttrs.imeOptions ^= imeActions;
              // set the DONE action
              outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
          }
          if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
              outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
          }
  
          outAttrs.actionId = EditorInfo.IME_ACTION_DONE;
  
          // Custom action labels are discouraged in L; a checkmark icon is shown in place of the
          // custom text in this case.
          outAttrs.actionLabel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? null :
              getContext().getString(R.string.action_label);
          return connection;
      }
  
      /*package*/ DrawableRecipientChip getLastChip() {
          DrawableRecipientChip last = null;
          DrawableRecipientChip[] chips = getSortedRecipients();
          if (chips != null && chips.length > 0) {
              last = chips[chips.length - 1];
          }
          return last;
      }
  
      /**
       * @return The list of {@link RecipientEntry}s that have been selected by the user.
       */
      public List<RecipientEntry> getSelectedRecipients() {
          DrawableRecipientChip[] chips =
                  getText().getSpans(0, getText().length(), DrawableRecipientChip.class);
          List<RecipientEntry> results = new ArrayList<RecipientEntry>();
          if (chips == null) {
              return results;
          }
  
          for (DrawableRecipientChip c : chips) {
              results.add(c.getEntry());
          }
  
          return results;
      }
  
      /**
       * @return The list of {@link RecipientEntry}s that have been selected by the user and also
       *         hidden due to {@link #mMoreChip} span.
       */
      public List<RecipientEntry> getAllRecipients() {
          List<RecipientEntry> results = getSelectedRecipients();
  
          if (mHiddenSpans != null) {
              for (DrawableRecipientChip chip : mHiddenSpans) {
                  results.add(chip.getEntry());
              }
          }
  
          return results;
      }
  
      @Override
      public void onSelectionChanged(int start, int end) {
          // When selection changes, see if it is inside the chips area.
          // If so, move the cursor back after the chips again.
          // Only exception is when we change the selection due to a selected chip.
          DrawableRecipientChip last = getLastChip();
          if (mSelectedChip == null && 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(Parcelable state) {
          Bundle savedInstanceState = (Bundle) state;
          if (!TextUtils.isEmpty(getText())) {
              super.onRestoreInstanceState(null);
          } else {
              super.onRestoreInstanceState(
                      savedInstanceState.getParcelable(STATE_TEXT_VIEW));
          }
          String savedWarningText = savedInstanceState.getString(
              STATE_CURRENT_WARNING_TEXT);
          if (!savedWarningText.isEmpty()) {
              showWarningDialog(savedWarningText);
          }
      }
  
      @Override
      public Parcelable onSaveInstanceState() {
          // If the user changes orientation while they are editing, just roll back the selection.
          clearSelectedChip();
          Bundle savedInstanceState = new Bundle();
          savedInstanceState.putParcelable(STATE_TEXT_VIEW, super.onSaveInstanceState());
          savedInstanceState.putString(STATE_CURRENT_WARNING_TEXT, mCurrentWarningText);
          return savedInstanceState;
      }
  
      /**
       * 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(CharSequence text, int start, 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(boolean hasFocus, int direction, Rect previous) {
          super.onFocusChanged(hasFocus, direction, previous);
          if (!hasFocus) {
              shrink();
          } else {
              expand();
          }
      }
  
      @Override
      public <T extends ListAdapter & Filterable> void setAdapter(@NonNull T adapter) {
          super.setAdapter(adapter);
          BaseRecipientAdapter baseAdapter = (BaseRecipientAdapter) adapter;
          baseAdapter.registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
              @Override
              public void onChanged(List<RecipientEntry> entries) {
                  int suggestionCount = entries == null ? 0 : entries.size();
  
                  // 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();
                      // Here the current suggestion count is still the old one since we update
                      // the count at the bottom of this function.
                      if (mCurrentSuggestionCount == 0) {
                          // Announce the new number of possible choices for accessibility.
                          announceForAccessibilityCompat(
                                  getSuggestionDropdownOpenedVerbalization(suggestionCount));
                      }
                  }
  
                  // Is the dropdown closing?
                  if ((entries == null || entries.size() == 0)
                          // Here the current suggestion count is still the old one since we update
                          // the count at the bottom of this function.
                          && mCurrentSuggestionCount != 0
                          // If there is no text, there's no need to know if no suggestions are
                          // available.
                          && getText().length() > 0) {
                      announceForAccessibilityCompat(getResources().getString(
                              R.string.accessbility_suggestion_dropdown_closed));
                  }
  
                  if ((entries != null)
                          && (entries.size() == 1)
                          && (entries.get(0).getEntryType() ==
                                  RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST)) {
                      // Do nothing; showing a single permissions entry. Resizing not required.
                  } else {
                      // Set the dropdown height to be the remaining height from the anchor to the
                      // bottom.
                      mDropdownAnchor.getLocationOnScreen(mCoords);
                      getWindowVisibleDisplayFrame(mRect);
                      setDropDownHeight(mRect.bottom - mCoords[1] - mDropdownAnchor.getHeight() -
                              getDropDownVerticalOffset());
                  }
  
                  mCurrentSuggestionCount = suggestionCount;
              }
          });
          baseAdapter.setDropdownChipLayouter(mDropdownChipLayouter);
      }
  
      /**
       * Return the accessibility verbalization when the suggestion dropdown is opened.
       */
      public String getSuggestionDropdownOpenedVerbalization(int suggestionCount) {
          return getResources().getString(R.string.accessbility_suggestion_dropdown_opened);
      }
  
      @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
      private void announceForAccessibilityCompat(String text) {
          final AccessibilityManager accessibilityManager =
                  (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
          final boolean isAccessibilityOn = accessibilityManager.isEnabled();
  
          if (isAccessibilityOn && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
              final ViewParent parent = getParent();
              if (parent != null) {
                  AccessibilityEvent event = AccessibilityEvent.obtain(
                          AccessibilityEvent.TYPE_ANNOUNCEMENT);
                  onInitializeAccessibilityEvent(event);
                  event.getText().add(text);
                  event.setContentDescription(null);
                  parent.requestSendAccessibilityEvent(this, event);
              }
          }
      }
  
      protected void scrollBottomIntoView() {
          if (mScrollView != null && mShouldShrink) {
              getLocationInWindow(mCoords);
              // 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 height = getHeight();
              final int currentPos = mCoords[1] + height;
              mScrollView.getLocationInWindow(mCoords);
              final int desiredPos = mCoords[1] + height / getLineCount();
              if (currentPos > desiredPos) {
                  mScrollView.scrollBy(0, currentPos - desiredPos);
              }
          }
      }
  
      protected ScrollView getScrollView() {
          return mScrollView;
      }
  
      @Override
      public void performValidation() {
          // Do nothing. Chips handles its own validation.
      }
  
      private void shrink() {
          if (mTokenizer == null) {
              return;
          }
          long contactId = mSelectedChip != null ? mSelectedChip.getEntry().getContactId() : -1;
          if (mSelectedChip != null && contactId != RecipientEntry.INVALID_CONTACT
                  && (!isPhoneQuery() && contactId != RecipientEntry.GENERATED_CONTACT)) {
              clearSelectedChip();
          } else {
              if (getWidth() <= 0) {
                  mHandler.removeCallbacks(mDelayedShrink);
  
                  if (getVisibility() == GONE) {
                      // We aren't going to have a width any time soon, so defer
                      // this until we're not GONE.
                      mRequiresShrinkWhenNotGone = true;
                  } else {
                      // 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.post(mDelayedShrink);
                  }
                  return;
              }
              // Reset any pending chips as they would have been handled
              // when the field lost focus.
              if (mPendingChipsCount > 0) {
                  postHandlePendingChips();
              } else {
                  Editable editable = getText();
                  int end = getSelectionEnd();
                  int start = mTokenizer.findTokenStart(editable, end);
                  DrawableRecipientChip[] chips =
                          getSpannable().getSpans(start, end, DrawableRecipientChip.class);
                  if ((chips == null || chips.length == 0)) {
                      Editable text = getText();
                      int whatEnd = mTokenizer.findTokenEnd(text, start);
                      // This token was already tokenized, so skip past the ending token.
                      if (whatEnd < text.length() && text.charAt(whatEnd) == ',') {
                          whatEnd = movePastTerminators(whatEnd);
                      }
                      // In the middle of chip; treat this as an edit
                      // and commit the whole token.
                      int selEnd = getSelectionEnd();
                      if (whatEnd != selEnd) {
                          handleEdit(start, whatEnd);
                      } else {
                          commitChip(start, end, editable);
                      }
                  }
              }
              mHandler.post(mAddTextWatcher);
          }
          createMoreChip();
      }
  
      private void expand() {
          if (mShouldShrink) {
              setMaxLines(Integer.MAX_VALUE);
          }
          removeMoreChip();
          setCursorVisible(true);
          Editable text = getText();
          setSelection(text != null && text.length() > 0 ? text.length() : 0);
          // If there are any temporary chips, try replacing them now that the user
          // has expanded the field.
          if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0) {
              new RecipientReplacementTask().execute();
              mTemporaryRecipients = null;
          }
      }
  
      private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) {
          paint.setTextSize(mChipFontSize);
          if (maxWidth <= 0 && Log.isLoggable(TAG, Log.DEBUG)) {
              Log.d(TAG, "Max width is negative: " + maxWidth);
          }
          return TextUtils.ellipsize(text, paint, maxWidth,
                  TextUtils.TruncateAt.END);
      }
  
      /**
       * Creates a bitmap of the given contact on a selected chip.
       *
       * @param contact The recipient entry to pull data from.
       * @param paint The paint to use to draw the bitmap.
       */
      private ChipBitmapContainer createChipBitmap(RecipientEntry contact, TextPaint paint) {
          paint.setColor(getDefaultChipTextColor(contact));
          ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint,
                  getChipBackground(contact), getDefaultChipBackgroundColor(contact));
  
          if (bitmapContainer.loadIcon) {
              loadAvatarIcon(contact, bitmapContainer);
          }
          return bitmapContainer;
      }
  
      private ChipBitmapContainer createChipBitmap(RecipientEntry contact, TextPaint paint,
              Drawable overrideBackgroundDrawable, int backgroundColor) {
          final ChipBitmapContainer result = new ChipBitmapContainer();
  
          Drawable indicatorIcon = null;
          int indicatorPadding = 0;
          if (contact.getIndicatorIconId() != 0) {
              indicatorIcon = getContext().getDrawable(contact.getIndicatorIconId());
              indicatorIcon.setBounds(0, 0,
                      indicatorIcon.getIntrinsicWidth(), indicatorIcon.getIntrinsicHeight());
              indicatorPadding = indicatorIcon.getBounds().width() + mChipTextEndPadding;
          }
  
          Rect backgroundPadding = new Rect();
          if (overrideBackgroundDrawable != null) {
              overrideBackgroundDrawable.getPadding(backgroundPadding);
          }
  
          // Ellipsize the text so that it takes AT MOST the entire width of the
          // autocomplete text entry area. Make sure to leave space for padding
          // on the sides.
          int height = (int) mChipHeight;
          // Since the icon is a square, it's width is equal to the maximum height it can be inside
          // the chip. Don't include iconWidth for invalid contacts and when not displaying photos.
          boolean displayIcon = contact.isValid() && contact.shouldDisplayIcon();
          int iconWidth = displayIcon ?
                  height - backgroundPadding.top - backgroundPadding.bottom : 0;
  
          final boolean shouldDisplayWarningIcon = mUntrustedAddresses.contains(
                  contact.getDestination());
          final float warningIconWidth = shouldDisplayWarningIcon ? mWarningIconHeight : 0;
          final float warningIconTopMargin = (mChipHeight - mWarningIconHeight) / 2f;
          final float warningIconEndMargin = shouldDisplayWarningIcon ? mChipTextEndPadding : 0;
  
          float[] widths = new float[1];
          paint.getTextWidths(" ", widths);
          CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
                  calculateAvailableWidth()
                      - iconWidth
                      - warningIconWidth
                      - warningIconEndMargin
                      - widths[0]
                      - backgroundPadding.left
                      - backgroundPadding.right
                      - indicatorPadding);
          int textWidth = (int) paint.measureText(ellipsizedText, 0, ellipsizedText.length());
  
          // Chip start padding is the same as the end padding if there is no contact image.
          final int startPadding = displayIcon ? mChipTextStartPadding : mChipTextEndPadding;
          // Make sure there is a minimum chip width so the user can ALWAYS
          // tap a chip without difficulty.
          int width = Math.max(iconWidth * 2,
                  textWidth
                      + startPadding
                      + mChipTextEndPadding
                      + iconWidth
                      + (int) warningIconWidth
                      + (int) warningIconEndMargin
                      + backgroundPadding.left
                      + backgroundPadding.right
                      + indicatorPadding);
  
          // Create the background of the chip.
          result.bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
          final Canvas canvas = new Canvas(result.bitmap);
  
          // Check if the background drawable is set via attr
          if (overrideBackgroundDrawable != null) {
              overrideBackgroundDrawable.setBounds(0, 0, width, height);
              overrideBackgroundDrawable.draw(canvas);
          } else {
              // Draw the default chip background
              mWorkPaint.reset();
              mWorkPaint.setColor(backgroundColor);
              final float radius = height / 2;
              canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius,
                      mWorkPaint);
          }
  
          // Draw the text vertically aligned
          int textX = shouldPositionAvatarOnRight() ?
                  mChipTextEndPadding
                      + backgroundPadding.left
                      + indicatorPadding
                      + (int) warningIconWidth
                      + (int) warningIconEndMargin :
                  width
                      - backgroundPadding.right
                      - mChipTextEndPadding
                      - textWidth
                      - indicatorPadding
                      - (int) warningIconWidth
                      - (int) warningIconEndMargin;
          canvas.drawText(ellipsizedText, 0, ellipsizedText.length(),
                  textX, getTextYOffset(height), paint);
  
          if (indicatorIcon != null) {
              int indicatorX = shouldPositionAvatarOnRight()
                  ? backgroundPadding.left + mChipTextEndPadding
                  : width - backgroundPadding.right - indicatorIcon.getBounds().width()
                          - mChipTextEndPadding;
              int indicatorY = height / 2 - indicatorIcon.getBounds().height() / 2;
              indicatorIcon.getBounds().offsetTo(indicatorX, indicatorY);
              indicatorIcon.draw(canvas);
          }
  
          // Set the variables that are needed to draw the icon bitmap once it's loaded
          final int iconX = shouldPositionAvatarOnRight() ?
                  width - backgroundPadding.right - iconWidth :
                  backgroundPadding.left;
          result.left = iconX;
          result.top = backgroundPadding.top;
          result.right = iconX + iconWidth;
          result.bottom = height - backgroundPadding.bottom;
          result.loadIcon = displayIcon;
  
          // Set the variables needed to draw the warning icon bitmap once it's loaded.
          final float warningIconX = shouldPositionAvatarOnRight() ?
                  backgroundPadding.left + warningIconEndMargin :
                  width - backgroundPadding.right - warningIconWidth - warningIconEndMargin;
          final float warningIconY = warningIconTopMargin;
          result.warningIconLeft = warningIconX;
          result.warningIconTop = warningIconY;
          result.warningIconRight = warningIconX + warningIconWidth;
          result.warningIconBottom = warningIconY + mWarningIconHeight;
  
          return result;
      }
  
      /**
       * Helper function that draws the loaded icon bitmap into the chips bitmap
       */
      private void drawIcon(ChipBitmapContainer bitMapResult, Bitmap icon) {
          if (icon == null) {
              return;
          }
          final Canvas canvas = new Canvas(bitMapResult.bitmap);
          final RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight());
          final RectF dst = new RectF(bitMapResult.left, bitMapResult.top, bitMapResult.right,
                  bitMapResult.bottom);
          drawCircularIconOnCanvas(icon, canvas, src, dst);
      }
  
      /**
       * Draws the warning icon onto the chip's bitmap and returns the rectangle it drew on.
       */
      private RectF drawWarningIcon(ChipBitmapContainer bitMapResult) {
          if (mWarningIcon == null) {
              return new RectF(0, 0, 0, 0);
          }
          final Canvas canvas = new Canvas(bitMapResult.bitmap);
          final RectF src = new RectF(0, 0, mWarningIcon.getWidth(), mWarningIcon.getHeight());
          final RectF dst = new RectF(bitMapResult.warningIconLeft, bitMapResult.warningIconTop,
                  bitMapResult.warningIconRight, bitMapResult.warningIconBottom);
          drawRectanglularIconOnCanvas(mWarningIcon, canvas, src, dst);
          return dst;
      }
  
      /**
       * Returns true if the avatar should be positioned at the right edge of the chip.
       * Takes into account both the set avatar position (start or end) as well as whether
       * the layout direction is LTR or RTL.
       */
      private boolean shouldPositionAvatarOnRight() {
          final boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
                  getLayoutDirection() == LAYOUT_DIRECTION_RTL;
          final boolean assignedPosition = mAvatarPosition == AVATAR_POSITION_END;
          // If in Rtl mode, the position should be flipped.
          return isRtl ? !assignedPosition : assignedPosition;
      }
  
      /**
       * Returns the avatar icon to use for this recipient entry. Returns null if we don't want to
       * draw an icon for this recipient.
       */
      private void loadAvatarIcon(final RecipientEntry contact,
              final ChipBitmapContainer bitmapContainer) {
          // Don't draw photos for recipients that have been typed in OR generated on the fly.
          long contactId = contact.getContactId();
          boolean drawPhotos = isPhoneQuery() ?
                  contactId != RecipientEntry.INVALID_CONTACT
                  : (contactId != RecipientEntry.INVALID_CONTACT
                          && contactId != RecipientEntry.GENERATED_CONTACT);
  
          if (drawPhotos) {
              final byte[] origPhotoBytes = contact.getPhotoBytes();
              // There may not be a photo yet if anything but the first contact address
              // was selected.
              if (origPhotoBytes == null) {
                  // TODO: cache this in the recipient entry?
                  getAdapter().fetchPhoto(contact, new PhotoManager.PhotoManagerCallback() {
                      @Override
                      public void onPhotoBytesPopulated() {
                          // Call through to the async version which will ensure
                          // proper threading.
                          onPhotoBytesAsynchronouslyPopulated();
                      }
  
                      @Override
                      public void onPhotoBytesAsynchronouslyPopulated() {
                          final byte[] loadedPhotoBytes = contact.getPhotoBytes();
                          final Bitmap icon = BitmapFactory.decodeByteArray(loadedPhotoBytes, 0,
                                  loadedPhotoBytes.length);
                          tryDrawAndInvalidate(icon);
                      }
  
                      @Override
                      public void onPhotoBytesAsyncLoadFailed() {
                          // TODO: can the scaled down default photo be cached?
                          tryDrawAndInvalidate(mDefaultContactPhoto);
                      }
  
                      private void tryDrawAndInvalidate(Bitmap icon) {
                          drawIcon(bitmapContainer, icon);
                          // The caller might originated from a background task. However, if the
                          // background task has already completed, the view might be already drawn
                          // on the UI but the callback would happen on the background thread.
                          // So if we are on a background thread, post an invalidate call to the UI.
                          if (Looper.myLooper() == Looper.getMainLooper()) {
                              // The view might not redraw itself since it's loaded asynchronously
                              invalidate();
                          } else {
                              post(new Runnable() {
                                  @Override
                                  public void run() {
                                      invalidate();
                                  }
                              });
                          }
                      }
                  });
              } else {
                  final Bitmap icon = BitmapFactory.decodeByteArray(origPhotoBytes, 0,
                          origPhotoBytes.length);
                  drawIcon(bitmapContainer, icon);
              }
          }
      }
  
      /**
       * Get the background drawable for a RecipientChip.
       */
      // Visible for testing.
      /* package */Drawable getChipBackground(RecipientEntry contact) {
          return contact.isValid() ? mChipBackground : mInvalidChipBackground;
      }
  
      private int getDefaultChipTextColor(RecipientEntry contact) {
          return contact.isValid() ? mUnselectedChipTextColor :
                  getResources().getColor(android.R.color.black);
      }
  
      private int getDefaultChipBackgroundColor(RecipientEntry contact) {
          return contact.isValid() ? mUnselectedChipBackgroundColor :
                  getResources().getColor(R.color.chip_background_invalid);
      }
  
      /**
       * Given a height, returns a Y offset that will draw the text in the middle of the height.
       */
      protected float getTextYOffset(int height) {
          return height - ((height - mTextHeight) / 2);
      }
  
      /**
       * Draws the icon onto the canvas given the source rectangle of the bitmap and the destination
       * rectangle of the canvas.
       *
       * <p>The icon is drawn as a circle.
       */
      protected void drawCircularIconOnCanvas(Bitmap icon, Canvas canvas, RectF src, RectF dst) {
          setWorkPaintForIcon(icon, src, dst);
          canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f, mWorkPaint);
  
          final float borderWidth = 1f;
          setWorkPaintForBorder(borderWidth);
          canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f - borderWidth / 2,
                  mWorkPaint);
  
          mWorkPaint.reset();
      }
  
      /**
       * Draws the icon onto the canvas given the source rectangle of the bitmap and the destination
       * rectangle of the canvas.
       *
       * <p>The icon is drawn as a rectangle.
       */
      private void drawRectanglularIconOnCanvas(Bitmap icon, Canvas canvas, RectF src, RectF dst) {
          setWorkPaintForIcon(icon, src, dst);
          canvas.drawRect(dst, mWorkPaint);
  
          final float borderWidth = 1f;
          setWorkPaintForBorder(borderWidth);
          canvas.drawRect(dst, mWorkPaint);
  
          mWorkPaint.reset();
      }
  
      /**
       * Sets WorkPaint for drawing the icon from src onto dst.
       */
      private void setWorkPaintForIcon(Bitmap icon, RectF src, RectF dst) {
          final Matrix matrix = new Matrix();
  
          // Draw bitmap through shader first.
          final BitmapShader shader = new BitmapShader(icon, TileMode.CLAMP, TileMode.CLAMP);
          matrix.reset();
  
          // Fit bitmap to bounds.
          matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);
  
          shader.setLocalMatrix(matrix);
          mWorkPaint.reset();
          mWorkPaint.setShader(shader);
          mWorkPaint.setAntiAlias(true);
          mWorkPaint.setFilterBitmap(true);
          mWorkPaint.setDither(true);
      }
  
      /**
       * Sets WorkPaint for drawing the icon border with the given width.
       */
      private void setWorkPaintForBorder(float borderWidth) {
          mWorkPaint.reset();
          mWorkPaint.setColor(Color.TRANSPARENT);
          mWorkPaint.setStyle(Style.STROKE);
          mWorkPaint.setStrokeWidth(borderWidth);
          mWorkPaint.setAntiAlias(true);
      }
  
      private DrawableRecipientChip constructChipSpan(RecipientEntry contact) {
          TextPaint paint = getPaint();
          float defaultSize = paint.getTextSize();
          int defaultColor = paint.getColor();
  
          ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint);
          final Rect warningIconBounds = new Rect(0, 0, 0, 0);
          if (mUntrustedAddresses.contains(contact.getDestination())) {
              drawWarningIcon(bitmapContainer).round(warningIconBounds);
          }
          final Bitmap tmpBitmap = bitmapContainer.bitmap;
  
          // Pass the full text, un-ellipsized, to the chip.
          final int iconWidth = tmpBitmap != null ? tmpBitmap.getWidth() : 0;
          final int iconHeight = tmpBitmap != null ? tmpBitmap.getHeight() : 0;
          Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
          result.setBounds(0, 0, iconWidth, iconHeight);
          VisibleRecipientChip recipientChip =
                  new VisibleRecipientChip(result, contact);
          recipientChip.setExtraMargin(mLineSpacingExtra);
          // Return text to the original size.
          paint.setTextSize(defaultSize);
          paint.setColor(defaultColor);
          // Put warning icon dimensions info in the chip
          recipientChip.setWarningIconBounds(warningIconBounds);
          return recipientChip;
      }
  
      /**
       * Calculate the offset from bottom of the EditText to top of the provided line.
       */
      private int calculateOffsetFromBottomToTop(int line) {
          return -(int) ((mChipHeight + (2 * mLineSpacingExtra)) * (Math
                  .abs(getLineCount() - line)) + getPaddingBottom());
      }
  
      /**
       * 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() - mChipTextStartPadding
                  - mChipTextEndPadding;
      }
  
  
      private void setChipDimensions(Context context, AttributeSet attrs) {
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecipientEditTextView, 0,
                  0);
          Resources r = getContext().getResources();
  
          mChipBackground = a.getDrawable(R.styleable.RecipientEditTextView_chipBackground);
          mInvalidChipBackground = a
                  .getDrawable(R.styleable.RecipientEditTextView_invalidChipBackground);
          mChipDelete = a.getDrawable(R.styleable.RecipientEditTextView_chipDelete);
          if (mChipDelete == null) {
              mChipDelete = r.getDrawable(R.drawable.ic_cancel_wht_24dp);
          }
          mChipTextStartPadding = mChipTextEndPadding
                  = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipPadding, -1);
          if (mChipTextStartPadding == -1) {
              mChipTextStartPadding = mChipTextEndPadding =
                      (int) r.getDimension(R.dimen.chip_padding);
          }
          // xml-overrides for each individual padding
          // TODO: add these to attr?
          int overridePadding = (int) r.getDimension(R.dimen.chip_padding_start);
          if (overridePadding >= 0) {
              mChipTextStartPadding = overridePadding;
          }
          overridePadding = (int) r.getDimension(R.dimen.chip_padding_end);
          if (overridePadding >= 0) {
              mChipTextEndPadding = overridePadding;
          }
  
          mDefaultContactPhoto = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture);
  
          mMoreItem = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.more_item, null);
  
          mChipHeight = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipHeight, -1);
          if (mChipHeight == -1) {
              mChipHeight = r.getDimension(R.dimen.chip_height);
          }
          mChipFontSize = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipFontSize, -1);
          if (mChipFontSize == -1) {
              mChipFontSize = r.getDimension(R.dimen.chip_text_size);
          }
          mAvatarPosition =
                  a.getInt(R.styleable.RecipientEditTextView_avatarPosition, AVATAR_POSITION_START);
          mDisableDelete = a.getBoolean(R.styleable.RecipientEditTextView_disableDelete, false);
  
          mMaxLines = r.getInteger(R.integer.chips_max_lines);
          mLineSpacingExtra = r.getDimensionPixelOffset(R.dimen.line_spacing_extra);
  
          mUnselectedChipTextColor = a.getColor(
                  R.styleable.RecipientEditTextView_unselectedChipTextColor,
                  r.getColor(android.R.color.black));
  
          mUnselectedChipBackgroundColor = a.getColor(
                  R.styleable.RecipientEditTextView_unselectedChipBackgroundColor,
                  r.getColor(R.color.chip_background));
  
          a.recycle();
      }
  
      // Visible for testing.
      /* package */ void setMoreItem(TextView moreItem) {
          mMoreItem = moreItem;
      }
  
  
      // Visible for testing.
      /* package */ void setChipBackground(Drawable chipBackground) {
          mChipBackground = chipBackground;
      }
  
      // Visible for testing.
      /* package */ void setChipHeight(int height) {
          mChipHeight = height;
      }
  
      public float getChipHeight() {
          return mChipHeight;
      }
  
      /** Returns whether view is in no-chip or chip mode. */
      public boolean isNoChipMode() {
          return mNoChipMode;
      }
  
      /**
       * 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(boolean shrink) {
          mShouldShrink = shrink;
      }
  
      @Override
      public void onSizeChanged(int width, int height, int oldw, int oldh) {
          super.onSizeChanged(width, height, oldw, oldh);
          if (width != 0 && height != 0) {
              if (mPendingChipsCount > 0) {
                  postHandlePendingChips();
              } else {
                  checkChipWidths();
              }
          }
          // Try to find the scroll view parent, if it exists.
          if (mScrollView == null && !mTriedGettingScrollView) {
              ViewParent parent = getParent();
              while (parent != null && !(parent instanceof ScrollView)) {
                  parent = parent.getParent();
              }
              if (parent != null) {
                  mScrollView = (ScrollView) parent;
              }
              mTriedGettingScrollView = true;
          }
      }
  
      private void postHandlePendingChips() {
          mHandler.removeCallbacks(mHandlePendingChips);
          mHandler.post(mHandlePendingChips);
      }
  
      private void checkChipWidths() {
          // Check the widths of the associated chips.
          DrawableRecipientChip[] chips = getSortedRecipients();
          if (chips != null) {
              Rect bounds;
              for (DrawableRecipientChip chip : chips) {
                  bounds = chip.getBounds();
                  if (getWidth() > 0 && bounds.right - bounds.left >
                          getWidth() - getPaddingLeft() - getPaddingRight()) {
                      // Need to redraw that chip.
                      replaceChip(chip, chip.getEntry());
                  }
              }
          }
      }
  
      // 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) {
              Editable editable = getText();
              // Tokenize!
              if (mPendingChipsCount <= MAX_CHIPS_PARSED) {
                  for (int i = 0; i < mPendingChips.size(); i++) {
                      String current = mPendingChips.get(i);
                      int tokenStart = editable.toString().indexOf(current);
                      // Always leave a space at the end between tokens.
                      int tokenEnd = tokenStart + current.length() - 1;
                      if (tokenStart >= 0) {
                          // When we have a valid token, include it with the token
                          // to the left.
                          if (tokenEnd < editable.length() - 2
                                  && editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) {
                              tokenEnd++;
                          }
                          createReplacementChip(tokenStart, tokenEnd, editable, i < CHIP_LIMIT
                                  || !mShouldShrink);
                      }
                      mPendingChipsCount--;
                  }
                  sanitizeEnd();
              } else {
                  mNoChipMode = 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.
          DrawableRecipientChip[] chips = getSortedRecipients();
          Spannable spannable = getSpannable();
          if (chips != null && chips.length > 0) {
              int end;
              mMoreChip = getMoreChip();
              if (mMoreChip != null) {
                  end = spannable.getSpanEnd(mMoreChip);
              } else {
                  end = getSpannable().getSpanEnd(getLastChip());
              }
              Editable editable = getText();
              int length = editable.length();
              if (length > end) {
                  // See what characters occur after that and eliminate them.
                  if (Log.isLoggable(TAG, Log.DEBUG)) {
                      Log.d(TAG, "There were extra characters after the last tokenizable entry."
                              + editable);
                  }
                  editable.delete(end + 1, length);
              }
          }
      }
  
      /**
       * 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(int tokenStart, int tokenEnd, Editable editable,
              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();
          int commitCharIndex = trimmedToken.lastIndexOf(COMMIT_CHAR_COMMA);
          if (commitCharIndex != -1 && commitCharIndex == trimmedToken.length() - 1) {
              token = trimmedToken.substring(0, trimmedToken.length() - 1);
          }
          RecipientEntry entry = createTokenizedEntry(token);
          if (entry != null) {
              DrawableRecipientChip chip = null;
              try {
                  if (!mNoChipMode) {
                      chip = visible ? constructChipSpan(entry) : new InvisibleRecipientChip(entry);
                  }
              } catch (NullPointerException e) {
                  Log.e(TAG, e.getMessage(), e);
              }
              editable.setSpan(chip, tokenStart, tokenEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
              // Add this chip to the list of entries "to replace"
              if (chip != null) {
                  if (mTemporaryRecipients == null) {
                      mTemporaryRecipients = new ArrayList<DrawableRecipientChip>();
                  }
                  chip.setOriginalText(token);
                  mTemporaryRecipients.add(chip);
              }
          }
      }
  
      // VisibleForTesting
      RecipientEntry createTokenizedEntry(final String token) {
          if (TextUtils.isEmpty(token)) {
              return null;
          }
          if (isPhoneQuery() && PhoneUtil.isPhoneNumber(token)) {
              return RecipientEntry.constructFakePhoneEntry(token, true);
          }
          Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token);
          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.
              String display = tokens[0].getName();
              if (!TextUtils.isEmpty(display)) {
                  return RecipientEntry.constructGeneratedEntry(display, tokens[0].getAddress(),
                          isValid);
              } else {
                  display = tokens[0].getAddress();
                  if (!TextUtils.isEmpty(display)) {
                      return RecipientEntry.constructFakeEntry(display, isValid);
                  }
              }
          }
          // Unable to validate the token or to create a valid token from it.
          // Just create a chip the user can edit.
          String validatedToken = null;
          if (mValidator != null && !isValid) {
              // Try fixing up the entry using the validator.
              validatedToken = mValidator.fixText(token).toString();
              if (!TextUtils.isEmpty(validatedToken)) {
                  if (validatedToken.contains(token)) {
                      // protect against the case of a validator with a null
                      // domain,
                      // which doesn't add a domain to the token
                      Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(validatedToken);
                      if (tokenized.length > 0) {
                          validatedToken = tokenized[0].getAddress();
                          isValid = true;
                      }
                  } else {
                      // We ran into a case where the token was invalid and
                      // removed
                      // by the validator. In this case, just use the original
                      // token
                      // and let the user sort out the error chip.
                      validatedToken = null;
                      isValid = false;
                  }
              }
          }
          // Otherwise, fallback to just creating an editable email address chip.
          return RecipientEntry.constructFakeEntry(
                  !TextUtils.isEmpty(validatedToken) ? validatedToken : token, isValid);
      }
  
      private boolean isValid(String text) {
          return mValidator == null ? true : mValidator.isValid(text);
      }
  
      private static String tokenizeAddress(String destination) {
          Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination);
          if (tokens != null && tokens.length > 0) {
              return tokens[0].getAddress();
          }
          return destination;
      }
  
      @Override
      public void setTokenizer(Tokenizer tokenizer) {
          mTokenizer = tokenizer;
          super.setTokenizer(mTokenizer);
      }
  
      @Override
      public void setValidator(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(CharSequence text) {
          return;
      }
  
      /**
       * Dismiss any selected chips when the back key is pressed.
       */
      @Override
      public boolean onKeyPreIme(int keyCode, @NonNull 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(int keyCode, @NonNull 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() {
          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 selected item (if one is selected), or the first item, in the popup
       * suggestions list. Otherwise, it is whatever the user had typed in. End represents where the
       * tokenizer should search for a token to turn into a chip.
       * @return If a chip was created from a real contact.
       */
      private boolean commitDefault() {
          // If there is no tokenizer, don't try to commit.
          if (mTokenizer == null) {
              return false;
          }
          Editable editable = getText();
          int end = getSelectionEnd();
          int start = mTokenizer.findTokenStart(editable, end);
  
          if (shouldCreateChip(start, end)) {
              int whatEnd = mTokenizer.findTokenEnd(getText(), start);
              // In the middle of chip; treat this as an edit
              // and commit the whole token.
              whatEnd = movePastTerminators(whatEnd);
              if (whatEnd != getSelectionEnd()) {
                  handleEdit(start, whatEnd);
                  return true;
              }
              return commitChip(start, end , editable);
          }
          return false;
      }
  
      private void commitByCharacter() {
          // We can't possibly commit by character if we can't tokenize.
          if (mTokenizer == null) {
              return;
          }
          Editable editable = getText();
          int end = getSelectionEnd();
          int start = mTokenizer.findTokenStart(editable, end);
          if (shouldCreateChip(start, end)) {
              commitChip(start, end, editable);
          }
          setSelection(getText().length());
      }
  
      private boolean commitChip(int start, int end, Editable editable) {
          int position = positionOfFirstEntryWithTypePerson();
          if (position != -1 && enoughToFilter()
                  && end == getSelectionEnd() && !isPhoneQuery()
                  && !isValidEmailAddress(editable.toString().substring(start, end).trim())) {
              // let's choose the selected or first entry if only the input text is NOT an email
              // address so we won't try to replace the user's potentially correct but
              // new/unencountered email input
              final int selectedPosition = getListSelection();
              if (selectedPosition == -1 || !isEntryAtPositionTypePerson(selectedPosition)) {
                  // Nothing is selected or selected item is not type person; use the first item
                  submitItemAtPosition(position);
              } else {
                  submitItemAtPosition(selectedPosition);
              }
              dismissDropDown();
              return true;
          } else {
              int tokenEnd = mTokenizer.findTokenEnd(editable, start);
              if (editable.length() > tokenEnd + 1) {
                  char charAt = editable.charAt(tokenEnd + 1);
                  if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) {
                      tokenEnd++;
                  }
              }
              String text = editable.toString().substring(start, tokenEnd).trim();
              clearComposingText();
              if (text.length() > 0 && !text.equals(" ")) {
                  RecipientEntry entry = createTokenizedEntry(text);
                  if (entry != null) {
                      QwertyKeyListener.markAsReplaced(editable, start, end, "");
                      CharSequence chipText = createChip(entry);
                      if (chipText != null && start > -1 && end > -1) {
                          editable.replace(start, end, chipText);
                      }
                  }
                  // Only dismiss the dropdown if it is related to the text we
                  // just committed.
                  // For paste, it may not be as there are possibly multiple
                  // tokens being added.
                  if (end == getSelectionEnd()) {
                      dismissDropDown();
                  }
                  sanitizeBetween();
                  return true;
              }
          }
          return false;
      }
  
      private int positionOfFirstEntryWithTypePerson() {
          ListAdapter adapter = getAdapter();
          int itemCount = adapter != null ? adapter.getCount() : 0;
          for (int i = 0; i < itemCount; i++) {
              if (isEntryAtPositionTypePerson(i)) {
                  return i;
              }
          }
          return -1;
      }
  
      private boolean isEntryAtPositionTypePerson(int position) {
          return getAdapter().getItem(position).getEntryType() == RecipientEntry.ENTRY_TYPE_PERSON;
      }
  
      // 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.
          DrawableRecipientChip[] recips = getSortedRecipients();
          if (recips != null && recips.length > 0) {
              DrawableRecipientChip last = recips[recips.length - 1];
              DrawableRecipientChip beforeLast = null;
              if (recips.length > 1) {
                  beforeLast = recips[recips.length - 2];
              }
              int startLooking = 0;
              int end = getSpannable().getSpanStart(last);
              if (beforeLast != null) {
                  startLooking = getSpannable().getSpanEnd(beforeLast);
                  Editable text = getText();
                  if (startLooking == -1 || startLooking > text.length() - 1) {
                      // There is nothing after this chip.
                      return;
                  }
                  if (text.charAt(startLooking) == ' ') {
                      startLooking++;
                  }
              }
              if (startLooking >= 0 && end >= 0 && startLooking < end) {
                  getText().delete(startLooking, end);
              }
          }
      }
  
      private boolean shouldCreateChip(int start, int end) {
          return !mNoChipMode && hasFocus() && enoughToFilter() && !alreadyHasChip(start, end);
      }
  
      private boolean alreadyHasChip(int start, int end) {
          if (mNoChipMode) {
              return true;
          }
          DrawableRecipientChip[] chips =
                  getSpannable().getSpans(start, end, DrawableRecipientChip.class);
          return chips != null && chips.length > 0;
      }
  
      private void handleEdit(int start, int end) {
          if (start == -1 || end == -1) {
              // This chip no longer exists in the field.
              dismissDropDown();
              return;
          }
          // This is in the middle of a chip, so select out the whole chip
          // and commit it.
          Editable editable = getText();
          setSelection(end);
          String text = getText().toString().substring(start, end);
          if (!TextUtils.isEmpty(text)) {
              RecipientEntry entry = RecipientEntry.constructFakeEntry(text, isValid(text));
              QwertyKeyListener.markAsReplaced(editable, start, end, "");
              CharSequence chipText = createChip(entry);
              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(int keyCode, @NonNull KeyEvent event) {
          if (mSelectedChip != null && keyCode == KeyEvent.KEYCODE_DEL) {
              if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
                  mAlternatesPopup.dismiss();
              }
              removeChip(mSelectedChip);
          }
  
          switch (keyCode) {
              case KeyEvent.KEYCODE_ENTER:
              case KeyEvent.KEYCODE_DPAD_CENTER:
                  if (event.hasNoModifiers()) {
                      if (commitDefault()) {
                          return true;
                      }
                      if (mSelectedChip != null) {
                          clearSelectedChip();
                          return true;
                      } else if (focusNext()) {
                          return true;
                      }
                  }
                  break;
          }
  
          final DrawableRecipientChip lastRecipientChip = getLastChip();
          boolean isHandled = super.onKeyDown(keyCode, event);
  
          /*
           * Hacky way to report a deleted chip:
           * In some devices/configurations, {@link KeyEvent#KEYCODE_DEL} character is causing
           * onKeyDown() to be called, which in turns handles the chip deletion instead of
           * {@link RecipientTextWatcher#onTextChanged}. We want to call
           * {@link RecipientChipDeletedListener#onRecipientChipDeleted} callback for these cases.
           */
          if (keyCode == KeyEvent.KEYCODE_DEL && isHandled && lastRecipientChip != null) {
              final RecipientEntry entry = lastRecipientChip.getEntry();
              if (!mNoChipMode && mRecipientChipDeletedListener != null && entry != null) {
                  mRecipientChipDeletedListener.onRecipientChipDeleted(entry);
              }
          }
  
          return isHandled;
      }
  
      // Visible for testing.
      /* package */ Spannable getSpannable() {
          return getText();
      }
  
      private int getChipStart(DrawableRecipientChip chip) {
          return getSpannable().getSpanStart(chip);
      }
  
      private int getChipEnd(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
      public void performFiltering(@NonNull CharSequence text, int keyCode) {
          boolean isCompletedToken = isCompletedToken(text);
          if (enoughToFilter() && !isCompletedToken) {
              int end = getSelectionEnd();
              int start = mTokenizer.findTokenStart(text, end);
              // If this is a RecipientChip, don't filter
              // on its contents.
              Spannable span = getSpannable();
              DrawableRecipientChip[] chips = span.getSpans(start, end, DrawableRecipientChip.class);
              if (chips != null && chips.length > 0) {
                  dismissDropDown();
                  return;
              }
          } else if (isCompletedToken) {
              dismissDropDown();
              return;
          }
          super.performFiltering(text, keyCode);
      }
  
      // Visible for testing.
      /*package*/ boolean isCompletedToken(CharSequence text) {
          if (TextUtils.isEmpty(text)) {
              return false;
          }
          // Check to see if this is a completed token before filtering.
          int end = text.length();
          int start = mTokenizer.findTokenStart(text, end);
          String token = text.toString().substring(start, end).trim();
          if (!TextUtils.isEmpty(token)) {
              char atEnd = token.charAt(token.length() - 1);
              return atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON;
          }
          return false;
      }
  
      /**
       * Clears the selected chip if there is one (and dismissing any popups related to the selected
       * chip in the process).
       */
      public void clearSelectedChip() {
          if (mSelectedChip != null) {
              unselectChip(mSelectedChip);
              mSelectedChip = null;
          }
          setCursorVisible(true);
          setSelection(getText().length());
      }
  
      /**
       * 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(@NonNull MotionEvent event) {
          boolean handled;
          int action = event.getAction();
          final float x = event.getX();
          final float y = event.getY();
          final int offset = putOffsetInRange(x, y);
          final DrawableRecipientChip currentChip = findChip(offset);
          if (action == MotionEvent.ACTION_UP) {
              boolean touchedWarningIcon = touchedWarningIcon(x, y, currentChip);
              if (touchedWarningIcon) {
                  String warningText = String.format(mWarningTextTemplate,
                      currentChip.getEntry().getDestination());
                  showWarningDialog(warningText);
                  return true;
              }
              if (!isFocused()) {
                  // Ignore further chip taps until this view is focused.
                  return touchedWarningIcon || super.onTouchEvent(event);
              }
              handled = super.onTouchEvent(event);
              if (mSelectedChip == null) {
                  mGestureDetector.onTouchEvent(event);
              }
              boolean chipWasSelected = false;
              if (currentChip != null) {
                  if (mSelectedChip != null && mSelectedChip != currentChip) {
                      clearSelectedChip();
                      selectChip(currentChip);
                  } else if (mSelectedChip == null) {
                      commitDefault();
                      selectChip(currentChip);
                  } else {
                      onClick(mSelectedChip);
                  }
                  chipWasSelected = true;
                  handled = true;
              } else if (mSelectedChip != null && shouldShowEditableText(mSelectedChip)) {
                  chipWasSelected = true;
              }
              if (!chipWasSelected) {
                  clearSelectedChip();
              }
          } else {
              boolean touchedWarningIcon = touchedWarningIcon(x, y, currentChip);
              if (touchedWarningIcon) {
                  return true;
              }
              handled = super.onTouchEvent(event);
              if (!isFocused()) {
                  return handled;
              }
              if (mSelectedChip == null) {
                  mGestureDetector.onTouchEvent(event);
              }
          }
          return handled;
      }
  
      private boolean touchedWarningIcon(float x, float y, DrawableRecipientChip currentChip) {
          boolean touchedWarningIcon = false;
          if (currentChip != null) {
              Rect outOfDomainWarningBounds = currentChip.getWarningIconBounds();
              if (outOfDomainWarningBounds != null) {
                  int chipLeftOffset = shouldPositionAvatarOnRight()
                          ? getChipEnd(currentChip) : getChipStart(currentChip);
                  float chipLeftPosition = this.getLayout().getPrimaryHorizontal(chipLeftOffset);
                  float chipTopPosition = this.getLayout().getLineTop(
                          this.getLayout().getLineForOffset(chipLeftOffset)) + getTotalPaddingTop();
                  final RectF touchOutOfDomainWarning = new RectF(
                          chipLeftPosition + outOfDomainWarningBounds.left,
                          chipTopPosition + outOfDomainWarningBounds.top,
                          chipLeftPosition + outOfDomainWarningBounds.right,
                          chipTopPosition + outOfDomainWarningBounds.bottom);
                  touchedWarningIcon = touchOutOfDomainWarning.contains(x, y);
              }
          }
          return touchedWarningIcon;
      }
  
      private void showWarningDialog(String warningText) {
          mCurrentWarningText = warningText;
          new AlertDialog.Builder(RecipientEditTextView.this.getContext())
                  .setTitle(mWarningTitle)
                  .setOnDismissListener(new DialogInterface.OnDismissListener() {
                      @Override
                      public void onDismiss(DialogInterface dialog) {
                          mCurrentWarningText = "";
                      }
                  })
                  .setMessage(mCurrentWarningText)
                  .show();
      }
  
      private void showAlternates(final DrawableRecipientChip currentChip,
              final ListPopupWindow alternatesPopup) {
          new AsyncTask<Void, Void, ListAdapter>() {
              @Override
              protected ListAdapter doInBackground(final Void... params) {
                  return createAlternatesAdapter(currentChip);
              }
  
              @Override
              protected void onPostExecute(final ListAdapter result) {
                  if (!mAttachedToWindow) {
                      return;
                  }
                  int line = getLayout().getLineForOffset(getChipStart(currentChip));
                  int bottomOffset = calculateOffsetFromBottomToTop(line);
  
                  // Align the alternates popup with the left side of the View,
                  // regardless of the position of the chip tapped.
                  alternatesPopup.setAnchorView((mAlternatePopupAnchor != null) ?
                          mAlternatePopupAnchor : RecipientEditTextView.this);
                  alternatesPopup.setVerticalOffset(bottomOffset);
                  alternatesPopup.setAdapter(result);
                  alternatesPopup.setOnItemClickListener(mAlternatesListener);
                  // Clear the checked item.
                  mCheckedItem = -1;
                  alternatesPopup.show();
                  ListView listView = alternatesPopup.getListView();
                  listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
                  // Checked item would be -1 if the adapter has not
                  // loaded the view that should be checked yet. The
                  // variable will be set correctly when onCheckedItemChanged
                  // is called in a separate thread.
                  if (mCheckedItem != -1) {
                      listView.setItemChecked(mCheckedItem, true);
                      mCheckedItem = -1;
                  }
              }
          }.execute((Void[]) null);
      }
  
      protected ListAdapter createAlternatesAdapter(DrawableRecipientChip chip) {
          return new RecipientAlternatesAdapter(getContext(), chip.getContactId(),
                  chip.getDirectoryId(), chip.getLookupKey(), chip.getDataId(),
                  getAdapter().getQueryType(), this, mDropdownChipLayouter,
                  constructStateListDeleteDrawable(), getAdapter().getPermissionsCheckListener());
      }
  
      private ListAdapter createSingleAddressAdapter(DrawableRecipientChip currentChip) {
          return new SingleRecipientArrayAdapter(getContext(), currentChip.getEntry(),
                  mDropdownChipLayouter, constructStateListDeleteDrawable());
      }
  
      private StateListDrawable constructStateListDeleteDrawable() {
          // Construct the StateListDrawable from deleteDrawable
          StateListDrawable deleteDrawable = new StateListDrawable();
          if (!mDisableDelete) {
              deleteDrawable.addState(new int[]{android.R.attr.state_activated}, mChipDelete);
          }
          deleteDrawable.addState(new int[0], null);
          return deleteDrawable;
      }
  
      @Override
      public void onCheckedItemChanged(int position) {
          ListView listView = mAlternatesPopup.getListView();
          if (listView != null && listView.getCheckedItemCount() == 0) {
              listView.setItemChecked(position, true);
          }
          mCheckedItem = position;
      }
  
      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(int o) {
          int offset = o;
          Editable text = getText();
          int length = text.length();
          // Remove whitespace from end to find "real end"
          int realLength = length;
          for (int i = length - 1; i >= 0; i--) {
              if (text.charAt(i) == ' ') {
                  realLength--;
              } else {
                  break;
              }
          }
  
          // If the offset is beyond or at the end of the text,
          // leave it alone.
          if (offset >= realLength) {
              return offset;
          }
          Editable editable = getText();
          while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
              // Keep walking backward!
              offset--;
          }
          return offset;
      }
  
      private static int findText(Editable text, int offset) {
          if (text.charAt(offset) != ' ') {
              return offset;
          }
          return -1;
      }
  
      private DrawableRecipientChip findChip(int offset) {
          final Spannable span = getSpannable();
          final DrawableRecipientChip[] chips =
                  span.getSpans(0, span.length(), DrawableRecipientChip.class);
          // Find the chip that contains this offset.
          for (DrawableRecipientChip chip : chips) {
              int start = getChipStart(chip);
              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(RecipientEntry entry) {
          String display = entry.getDisplayName();
          String address = entry.getDestination();
          if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
              display = null;
          }
          String trimmedDisplayText;
          if (isPhoneQuery() && PhoneUtil.isPhoneNumber(address)) {
              trimmedDisplayText = address.trim();
          } else {
              if (address != null) {
                  // Tokenize out the address in case the address already
                  // contained the username as well.
                  Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(address);
                  if (tokenized != null && tokenized.length > 0) {
                      address = tokenized[0].getAddress();
                  }
              }
              Rfc822Token token = new Rfc822Token(display, address, null);
              trimmedDisplayText = token.toString().trim();
          }
          int index = trimmedDisplayText.indexOf(",");
          return mTokenizer != null && !TextUtils.isEmpty(trimmedDisplayText)
                  && index < trimmedDisplayText.length() - 1 ? (String) mTokenizer
                  .terminateToken(trimmedDisplayText) : trimmedDisplayText;
      }
  
      // Visible for testing.
      // Use this method to generate text to display in a chip.
      /*package*/ String createChipDisplayText(RecipientEntry entry) {
          String display = entry.getDisplayName();
          String address = entry.getDestination();
          if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
              display = null;
          }
          if (!TextUtils.isEmpty(display)) {
              return display;
          } else if (!TextUtils.isEmpty(address)){
              return address;
          } else {
              return new Rfc822Token(display, address, null).toString();
          }
      }
  
      private CharSequence createChip(RecipientEntry entry) {
          final String displayText = createAddressText(entry);
          if (TextUtils.isEmpty(displayText)) {
              return null;
          }
          // Always leave a blank space at the end of a chip.
          final int textLength = displayText.length() - 1;
          final SpannableString  chipText = new SpannableString(displayText);
          if (!mNoChipMode) {
              try {
                  DrawableRecipientChip chip = constructChipSpan(entry);
                  chipText.setSpan(chip, 0, textLength,
                          Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                  chip.setOriginalText(chipText.toString());
              } catch (NullPointerException e) {
                  Log.e(TAG, e.getMessage(), e);
                  return null;
              }
          }
          onChipCreated(entry);
          return chipText;
      }
  
      /**
       * A callback for subclasses to use to know when a chip was created with the
       * given RecipientEntry.
       */
      protected void onChipCreated(RecipientEntry entry) {
          if (!mNoChipMode && mRecipientChipAddedListener != null) {
              mRecipientChipAddedListener.onRecipientChipAdded(entry);
          }
      }
  
      /**
       * When an item in the suggestions list has been clicked, create a chip from the
       * contact information of the selected item.
       */
      @Override
      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
          if (position < 0) {
              return;
          }
  
          final RecipientEntry entry = getAdapter().getItem(position);
          if (entry.getEntryType() == RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST) {
              if (mPermissionsRequestItemClickedListener != null) {
                  mPermissionsRequestItemClickedListener
                          .onPermissionsRequestItemClicked(this, entry.getPermissions());
              }
              return;
          }
  
          final int charactersTyped = submitItemAtPosition(position);
          if (charactersTyped > -1 && mRecipientEntryItemClickedListener != null) {
              mRecipientEntryItemClickedListener
                      .onRecipientEntryItemClicked(charactersTyped, position);
          }
      }
  
      private int submitItemAtPosition(int position) {
          RecipientEntry entry = createValidatedEntry(getAdapter().getItem(position));
          if (entry == null) {
              return -1;
          }
          clearComposingText();
  
          int end = getSelectionEnd();
          int start = mTokenizer.findTokenStart(getText(), end);
  
          Editable editable = getText();
          QwertyKeyListener.markAsReplaced(editable, start, end, "");
          CharSequence chip = createChip(entry);
          if (chip != null && start >= 0 && end >= 0) {
              editable.replace(start, end, chip);
          }
          sanitizeBetween();
  
          return end - start;
      }
  
      private RecipientEntry createValidatedEntry(RecipientEntry item) {
          if (item == null) {
              return null;
          }
          final RecipientEntry entry;
          // If the display name and the address are the same, or if this is a
          // valid contact, but the destination is invalid, then make this a fake
          // recipient that is editable.
          String destination = item.getDestination();
          if (!isPhoneQuery() && item.getContactId() == RecipientEntry.GENERATED_CONTACT) {
              entry = RecipientEntry.constructGeneratedEntry(item.getDisplayName(),
                      destination, item.isValid());
          } else if (RecipientEntry.isCreatedRecipient(item.getContactId())
                  && (TextUtils.isEmpty(item.getDisplayName())
                          || TextUtils.equals(item.getDisplayName(), destination)
                          || (mValidator != null && !mValidator.isValid(destination)))) {
              entry = RecipientEntry.constructFakeEntry(destination, item.isValid());
          } else {
              entry = item;
          }
          return entry;
      }
  
      // Visible for testing.
      /* package */DrawableRecipientChip[] getSortedRecipients() {
          DrawableRecipientChip[] recips = getSpannable()
                  .getSpans(0, getText().length(), DrawableRecipientChip.class);
          ArrayList<DrawableRecipientChip> recipientsList = new ArrayList<DrawableRecipientChip>(
                  Arrays.asList(recips));
          final Spannable spannable = getSpannable();
          Collections.sort(recipientsList, new Comparator<DrawableRecipientChip>() {
  
              @Override
              public int compare(DrawableRecipientChip first, DrawableRecipientChip second) {
                  int firstStart = spannable.getSpanStart(first);
                  int secondStart = spannable.getSpanStart(second);
                  if (firstStart < secondStart) {
                      return -1;
                  } else if (firstStart > secondStart) {
                      return 1;
                  } else {
                      return 0;
                  }
              }
          });
          return recipientsList.toArray(new DrawableRecipientChip[recipientsList.size()]);
      }
  
      @Override
      public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
          return false;
      }
  
      @Override
      public void onDestroyActionMode(ActionMode mode) {
      }
  
      @Override
      public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
          return false;
      }
  
      /**
       * No chips are selectable.
       */
      @Override
      public boolean onCreateActionMode(ActionMode mode, Menu menu) {
          return false;
      }
  
      // Visible for testing.
      /* package */ReplacementDrawableSpan getMoreChip() {
          MoreImageSpan[] moreSpans = getSpannable().getSpans(0, getText().length(),
                  MoreImageSpan.class);
          return moreSpans != null && moreSpans.length > 0 ? moreSpans[0] : null;
      }
  
      private MoreImageSpan createMoreSpan(int count) {
          String moreText = String.format(mMoreItem.getText().toString(), count);
          mWorkPaint.set(getPaint());
          mWorkPaint.setTextSize(mMoreItem.getTextSize());
          mWorkPaint.setColor(mMoreItem.getCurrentTextColor());
          final int width = (int) mWorkPaint.measureText(moreText) + mMoreItem.getPaddingLeft()
                  + mMoreItem.getPaddingRight();
          final int height = (int) mChipHeight;
          Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
          Canvas canvas = new Canvas(drawable);
          int adjustedHeight = height;
          Layout layout = getLayout();
          if (layout != null) {
              adjustedHeight -= layout.getLineDescent(0);
          }
          canvas.drawText(moreText, 0, moreText.length(), 0, adjustedHeight, mWorkPaint);
  
          Drawable result = new BitmapDrawable(getResources(), drawable);
          result.setBounds(0, 0, width, height);
          return new MoreImageSpan(result);
      }
  
      // Visible for testing.
      /*package*/ void createMoreChipPlainText() {
          // Take the first <= CHIP_LIMIT addresses and get to the end of the second one.
          Editable text = getText();
          int start = 0;
          int end = start;
          for (int i = 0; i < CHIP_LIMIT; i++) {
              end = movePastTerminators(mTokenizer.findTokenEnd(text, start));
              start = end; // move to the next token and get its end.
          }
          // Now, count total addresses.
          int tokenCount = countTokens(text);
          MoreImageSpan moreSpan = createMoreSpan(tokenCount - CHIP_LIMIT);
          SpannableString chipText = new SpannableString(text.subSequence(end, text.length()));
          chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          text.replace(end, text.length(), chipText);
          mMoreChip = moreSpan;
      }
  
      // Visible for testing.
      /* package */int countTokens(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 (mNoChipMode) {
              createMoreChipPlainText();
              return;
          }
  
          if (!mShouldShrink) {
              return;
          }
          ReplacementDrawableSpan[] tempMore = getSpannable().getSpans(0, getText().length(),
                  MoreImageSpan.class);
          if (tempMore.length > 0) {
              getSpannable().removeSpan(tempMore[0]);
          }
          DrawableRecipientChip[] recipients = getSortedRecipients();
  
          if (recipients == null || recipients.length <= CHIP_LIMIT) {
              mMoreChip = null;
              return;
          }
          Spannable spannable = getSpannable();
          int numRecipients = recipients.length;
          int overage = numRecipients - CHIP_LIMIT;
          MoreImageSpan moreSpan = createMoreSpan(overage);
          mHiddenSpans = new ArrayList<DrawableRecipientChip>();
          int totalReplaceStart = 0;
          int totalReplaceEnd = 0;
          Editable text = getText();
          for (int i = numRecipients - overage; i < recipients.length; i++) {
              mHiddenSpans.add(recipients[i]);
              if (i == numRecipients - overage) {
                  totalReplaceStart = spannable.getSpanStart(recipients[i]);
              }
              if (i == recipients.length - 1) {
                  totalReplaceEnd = spannable.getSpanEnd(recipients[i]);
              }
              if (mTemporaryRecipients == null || !mTemporaryRecipients.contains(recipients[i])) {
                  int spanStart = spannable.getSpanStart(recipients[i]);
                  int spanEnd = spannable.getSpanEnd(recipients[i]);
                  recipients[i].setOriginalText(text.toString().substring(spanStart, spanEnd));
              }
              spannable.removeSpan(recipients[i]);
          }
          if (totalReplaceEnd < text.length()) {
              totalReplaceEnd = text.length();
          }
          int end = Math.max(totalReplaceStart, totalReplaceEnd);
          int start = Math.min(totalReplaceStart, totalReplaceEnd);
          SpannableString chipText = new SpannableString(text.subSequence(start, end));
          chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          text.replace(start, end, chipText);
          mMoreChip = moreSpan;
          // If adding the +more chip goes over the limit, resize accordingly.
          if (!isPhoneQuery() && getLineCount() > mMaxLines) {
              setMaxLines(getLineCount());
          }
      }
  
      /**
       * 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) {
              Spannable span = getSpannable();
              span.removeSpan(mMoreChip);
              mMoreChip = null;
              // Re-add the spans that were hidden.
              if (mHiddenSpans != null && mHiddenSpans.size() > 0) {
                  // Recreate each hidden span.
                  DrawableRecipientChip[] recipients = getSortedRecipients();
                  // Start the search for tokens after the last currently visible
                  // chip.
                  if (recipients == null || recipients.length == 0) {
                      return;
                  }
                  int end = span.getSpanEnd(recipients[recipients.length - 1]);
                  Editable editable = getText();
                  for (DrawableRecipientChip chip : mHiddenSpans) {
                      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 hidden spans,
                      // reduce the size of the string we need to search.
                      // That way, if there are duplicates, we always find the correct
                      // recipient.
                      chipStart = editable.toString().indexOf(token, end);
                      end = chipEnd = Math.min(editable.length(), chipStart + token.length());
                      // Only set the span if we found a matching token.
                      if (chipStart != -1) {
                          editable.setSpan(chip, chipStart, chipEnd,
                                  Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                      }
                  }
                  mHiddenSpans.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 show a popup window with the address in use highlighted and any other
       * alternate addresses for the contact.
       * @param currentChip Chip to select.
       */
      private void selectChip(DrawableRecipientChip currentChip) {
          if (shouldShowEditableText(currentChip)) {
              CharSequence text = currentChip.getValue();
              Editable editable = getText();
              Spannable spannable = getSpannable();
              int spanStart = spannable.getSpanStart(currentChip);
              int spanEnd = spannable.getSpanEnd(currentChip);
              spannable.removeSpan(currentChip);
              // Don't need leading space if it's the only chip
              if (spanEnd - spanStart == editable.length() - 1) {
                  spanEnd++;
              }
              editable.delete(spanStart, spanEnd);
              setCursorVisible(true);
              setSelection(editable.length());
              editable.append(text);
              mSelectedChip = constructChipSpan(
                      RecipientEntry.constructFakeEntry((String) text, isValid(text.toString())));
  
              /*
               * Because chip is destroyed and converted into an editable text, we call
               * {@link RecipientChipDeletedListener#onRecipientChipDeleted}. For the cases where
               * editable text is not shown (i.e. chip is in user's contact list), chip is focused
               * and below callback is not called.
               */
              if (!mNoChipMode && mRecipientChipDeletedListener != null) {
                  mRecipientChipDeletedListener.onRecipientChipDeleted(currentChip.getEntry());
              }
          } else {
              final boolean showAddress =
                      currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT ||
                      getAdapter().forceShowAddress();
              if (showAddress && mNoChipMode) {
                  return;
              }
  
              if (isTouchExplorationEnabled()) {
                  // The chips cannot be touch-explored. However, doing a double-tap results in
                  // the popup being shown for the last chip, which is of no value.
                  return;
              }
  
              mSelectedChip = currentChip;
              setSelection(getText().getSpanEnd(mSelectedChip));
              setCursorVisible(false);
  
              if (showAddress) {
                  showAddress(currentChip, mAddressPopup);
              } else {
                  showAlternates(currentChip, mAlternatesPopup);
              }
          }
      }
  
      private boolean isTouchExplorationEnabled() {
          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
              return false;
          }
  
          final AccessibilityManager accessibilityManager = (AccessibilityManager)
                  getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
          return accessibilityManager.isTouchExplorationEnabled();
      }
  
      private boolean shouldShowEditableText(DrawableRecipientChip currentChip) {
          long contactId = currentChip.getContactId();
          return contactId == RecipientEntry.INVALID_CONTACT
                  || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
      }
  
      private void showAddress(final DrawableRecipientChip currentChip, final ListPopupWindow popup) {
          if (!mAttachedToWindow) {
              return;
          }
          int line = getLayout().getLineForOffset(getChipStart(currentChip));
          int bottomOffset = calculateOffsetFromBottomToTop(line);
          // Align the alternates popup with the left side of the View,
          // regardless of the position of the chip tapped.
          popup.setAnchorView((mAlternatePopupAnchor != null) ? mAlternatePopupAnchor : this);
          popup.setVerticalOffset(bottomOffset);
          popup.setAdapter(createSingleAddressAdapter(currentChip));
          popup.setOnItemClickListener(new OnItemClickListener() {
              @Override
              public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                  unselectChip(currentChip);
                  popup.dismiss();
              }
          });
          popup.show();
          ListView listView = popup.getListView();
          listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
          listView.setItemChecked(0, true);
      }
  
      /**
       * 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(DrawableRecipientChip chip) {
          int start = getChipStart(chip);
          int end = getChipEnd(chip);
          Editable editable = getText();
          mSelectedChip = null;
          if (start == -1 || end == -1) {
              Log.w(TAG, "The chip doesn't exist or may be a chip a user was editing");
              setSelection(editable.length());
              commitDefault();
          } else {
              getSpannable().removeSpan(chip);
              QwertyKeyListener.markAsReplaced(editable, start, end, "");
              editable.removeSpan(chip);
              try {
                  if (!mNoChipMode) {
                      editable.setSpan(constructChipSpan(chip.getEntry()),
                              start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                  }
              } catch (NullPointerException e) {
                  Log.e(TAG, e.getMessage(), e);
              }
          }
          setCursorVisible(true);
          setSelection(editable.length());
          if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
              mAlternatesPopup.dismiss();
          }
      }
  
      @Override
      public void onChipDelete() {
          if (mSelectedChip != null) {
              if (!mNoChipMode && mRecipientChipDeletedListener != null) {
                  mRecipientChipDeletedListener.onRecipientChipDeleted(mSelectedChip.getEntry());
              }
              removeChip(mSelectedChip);
          }
          dismissPopups();
      }
  
      @Override
      public void onPermissionRequestDismissed() {
          if (mPermissionsRequestItemClickedListener != null) {
              mPermissionsRequestItemClickedListener.onPermissionRequestDismissed();
          }
          dismissDropDown();
      }
  
      private void dismissPopups() {
          if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
              mAlternatesPopup.dismiss();
          }
          if (mAddressPopup != null && mAddressPopup.isShowing()) {
              mAddressPopup.dismiss();
          }
          setSelection(getText().length());
      }
  
      /**
       * Remove the chip and any text associated with it from the RecipientEditTextView.
       */
      // Visible for testing.
      /* package */void removeChip(DrawableRecipientChip chip) {
          Spannable spannable = getSpannable();
          int spanStart = spannable.getSpanStart(chip);
          int spanEnd = spannable.getSpanEnd(chip);
          Editable text = getText();
          int toDelete = spanEnd;
          boolean wasSelected = chip == mSelectedChip;
          // Clear that there is a selected chip before updating any text.
          if (wasSelected) {
              mSelectedChip = null;
          }
          // Always remove trailing spaces when removing a chip.
          while (toDelete >= 0 && toDelete < text.length() && text.charAt(toDelete) == ' ') {
              toDelete++;
          }
          spannable.removeSpan(chip);
          if (spanStart >= 0 && toDelete > 0) {
              text.delete(spanStart, toDelete);
          }
          if (wasSelected) {
              clearSelectedChip();
          }
      }
  
      /**
       * Replace this currently selected chip with a new chip
       * that uses the contact data provided.
       */
      // Visible for testing.
      /*package*/ void replaceChip(DrawableRecipientChip chip, RecipientEntry entry) {
          boolean wasSelected = chip == mSelectedChip;
          if (wasSelected) {
              mSelectedChip = null;
          }
          int start = getChipStart(chip);
          int end = getChipEnd(chip);
          getSpannable().removeSpan(chip);
          Editable editable = getText();
          entry.setInReplacedChip(true);
          CharSequence chipText = createChip(entry);
          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();
          }
      }
  
      /**
       * 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(DrawableRecipientChip chip) {
          if (chip.isSelected()) {
              clearSelectedChip();
          }
      }
  
      private boolean chipsPending() {
          return mPendingChipsCount > 0 || (mHiddenSpans != null && mHiddenSpans.size() > 0);
      }
  
      @Override
      public void removeTextChangedListener(TextWatcher watcher) {
          mTextWatcher = null;
          super.removeTextChangedListener(watcher);
      }
  
      private boolean isValidEmailAddress(String input) {
          return !TextUtils.isEmpty(input) && mValidator != null &&
                  mValidator.isValid(input);
      }
  
      private class RecipientTextWatcher implements TextWatcher {
  
          @Override
          public void afterTextChanged(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.
                  Spannable spannable = getSpannable();
                  DrawableRecipientChip[] chips = spannable.getSpans(0, getText().length(),
                          DrawableRecipientChip.class);
                  for (DrawableRecipientChip chip : chips) {
                      spannable.removeSpan(chip);
                  }
                  if (mMoreChip != null) {
                      spannable.removeSpan(mMoreChip);
                  }
                  clearSelectedChip();
                  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;
                  }
              }
              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;
                  int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
                  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.
                          String text = getText().toString();
                          int tokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
                          String sub = text.substring(tokenStart, mTokenizer.findTokenEnd(text,
                                  tokenStart));
                          if (isValidEmailAddress(sub)) {
                              commitByCharacter();
                          }
                      }
                  }
              }
          }
  
          @Override
          public void onTextChanged(CharSequence s, int start, int before, 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 (before - count == 1) {
                  // If the item deleted is a space, and the thing before the
                  // space is a chip, delete the entire span.
                  int selStart = getSelectionStart();
                  DrawableRecipientChip[] repl = getSpannable().getSpans(selStart, selStart,
                          DrawableRecipientChip.class);
                  if (repl.length > 0) {
                      // There is a chip there! Just remove it.
                      DrawableRecipientChip toDelete = repl[0];
                      Editable editable = getText();
                      // Add the separator token.
                      int deleteStart = editable.getSpanStart(toDelete);
                      int deleteEnd = editable.getSpanEnd(toDelete) + 1;
                      if (deleteEnd > editable.length()) {
                          deleteEnd = editable.length();
                      }
                      if (!mNoChipMode && mRecipientChipDeletedListener != null) {
                          mRecipientChipDeletedListener.onRecipientChipDeleted(toDelete.getEntry());
                      }
                      editable.removeSpan(toDelete);
                      editable.delete(deleteStart, deleteEnd);
                  }
              } else if (count > before) {
                  if (mSelectedChip != null
                      && isGeneratedContact(mSelectedChip)) {
                      if (lastCharacterIsCommitCharacter(s)) {
                          commitByCharacter();
                          return;
                      }
                  }
              }
          }
  
          @Override
          public void beforeTextChanged(CharSequence s, int start, int count, int after) {
              // Do nothing.
          }
      }
  
      public boolean lastCharacterIsCommitCharacter(CharSequence s) {
          char last;
          int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
          int len = length() - 1;
          if (end != len) {
              last = s.charAt(end);
          } else {
              last = s.charAt(len);
          }
          return last == COMMIT_CHAR_COMMA || last == COMMIT_CHAR_SEMICOLON;
      }
  
      public boolean isGeneratedContact(DrawableRecipientChip chip) {
          long contactId = chip.getContactId();
          return contactId == RecipientEntry.INVALID_CONTACT
                  || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
      }
  
      /**
       * Handles pasting a {@link ClipData} to this {@link RecipientEditTextView}.
       */
      // Visible for testing.
      void handlePasteClip(ClipData clip) {
          if (clip == null) {
              // Do nothing.
              return;
          }
  
          final ClipDescription clipDesc = clip.getDescription();
          boolean containsSupportedType = clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)
                  || clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
          if (!containsSupportedType) {
              return;
          }
  
          removeTextChangedListener(mTextWatcher);
  
          final ClipDescription clipDescription = clip.getDescription();
          for (int i = 0; i < clip.getItemCount(); i++) {
              final String mimeType = clipDescription.getMimeType(i);
              final boolean supportedType = ClipDescription.MIMETYPE_TEXT_PLAIN.equals(mimeType)
                      || ClipDescription.MIMETYPE_TEXT_HTML.equals(mimeType);
              if (!supportedType) {
                  // Only plain text and html can be pasted.
                  continue;
              }
  
              final CharSequence pastedItem = clip.getItemAt(i).getText();
              if (!TextUtils.isEmpty(pastedItem)) {
                  final Editable editable = getText();
                  final int start = getSelectionStart();
                  final int end = getSelectionEnd();
                  if (start < 0 || end < 1) {
                      // No selection.
                      editable.append(pastedItem);
                  } else if (start == end) {
                      // Insert at position.
                      editable.insert(start, pastedItem);
                  } else {
                      editable.append(pastedItem, start, end);
                  }
                  handlePasteAndReplace();
              }
          }
  
          mHandler.post(mAddTextWatcher);
      }
  
      @Override
      public boolean onTextContextMenuItem(int id) {
          if (id == android.R.id.paste) {
              ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
                      Context.CLIPBOARD_SERVICE);
              handlePasteClip(clipboard.getPrimaryClip());
              return true;
          }
          return super.onTextContextMenuItem(id);
      }
  
      private void handlePasteAndReplace() {
          ArrayList<DrawableRecipientChip> created = handlePaste();
          if (created != null && created.size() > 0) {
              // Perform reverse lookups on the pasted contacts.
              IndividualReplacementTask replace = new IndividualReplacementTask();
              replace.execute(created);
          }
      }
  
      // Visible for testing.
      /* package */ArrayList<DrawableRecipientChip> handlePaste() {
          String text = getText().toString();
          int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
          String lastAddress = text.substring(originalTokenStart);
          int tokenStart = originalTokenStart;
          int prevTokenStart = 0;
          DrawableRecipientChip findChip = null;
          ArrayList<DrawableRecipientChip> created = new ArrayList<DrawableRecipientChip>();
          if (tokenStart != 0) {
              // There are things before this!
              while (tokenStart != 0 && findChip == null && tokenStart != prevTokenStart) {
                  prevTokenStart = tokenStart;
                  tokenStart = mTokenizer.findTokenStart(text, tokenStart);
                  findChip = findChip(tokenStart);
                  if (tokenStart == originalTokenStart && findChip == null) {
                      break;
                  }
              }
              if (tokenStart != originalTokenStart) {
                  if (findChip != null) {
                      tokenStart = prevTokenStart;
                  }
                  int tokenEnd;
                  DrawableRecipientChip createdChip;
                  while (tokenStart < originalTokenStart) {
                      tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(getText().toString(),
                              tokenStart));
                      commitChip(tokenStart, tokenEnd, getText());
                      createdChip = findChip(tokenStart);
                      if (createdChip == null) {
                          break;
                      }
                      // +1 for the space at the end.
                      tokenStart = getSpannable().getSpanEnd(createdChip) + 1;
                      created.add(createdChip);
                  }
              }
          }
          // Take a look at the last token. If the token has been completed with a
          // commit character, create a chip.
          if (isCompletedToken(lastAddress)) {
              Editable editable = getText();
              tokenStart = editable.toString().indexOf(lastAddress, originalTokenStart);
              commitChip(tokenStart, editable.length(), editable);
              created.add(findChip(tokenStart));
          }
          return created;
      }
  
      // Visible for testing.
      /* package */int movePastTerminators(int tokenEnd) {
          if (tokenEnd >= length()) {
              return tokenEnd;
          }
          char atEnd = getText().toString().charAt(tokenEnd);
          if (atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON) {
              tokenEnd++;
          }
          // This token had not only an end token character, but also a space
          // separating it from the next token.
          if (tokenEnd < length() && getText().toString().charAt(tokenEnd) == ' ') {
              tokenEnd++;
          }
          return tokenEnd;
      }
  
      private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> {
          private DrawableRecipientChip createFreeChip(RecipientEntry entry) {
              try {
                  if (mNoChipMode) {
                      return null;
                  }
                  return constructChipSpan(entry);
              } catch (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();
              Collections.addAll(originalRecipients, existingChips);
              if (mHiddenSpans != null) {
                  originalRecipients.addAll(mHiddenSpans);
              }
  
              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(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>();
              DrawableRecipientChip[] existingChips = getSortedRecipients();
              Collections.addAll(recipients, existingChips);
              if (mHiddenSpans != null) {
                  recipients.addAll(mHiddenSpans);
              }
              ArrayList<String> addresses = new ArrayList<String>();
              for (DrawableRecipientChip chip : recipients) {
                  if (chip != null) {
                      addresses.add(createAddressText(chip.getEntry()));
                  }
              }
              final BaseRecipientAdapter adapter = getAdapter();
              adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
                          @Override
                          public void matchesFound(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);
                  }
              }
          }
      }
  
      private class IndividualReplacementTask
              extends AsyncTask<ArrayList<DrawableRecipientChip>, Void, Void> {
          @Override
          protected Void doInBackground(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];
              ArrayList<String> addresses = new ArrayList<String>();
              for (DrawableRecipientChip chip : originalRecipients) {
                  if (chip != null) {
                      addresses.add(createAddressText(chip.getEntry()));
                  }
              }
              final BaseRecipientAdapter adapter = getAdapter();
              adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
  
                          @Override
                          public void matchesFound(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()));
                                      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;
          }
      }
  
  
      /**
       * MoreImageSpan is a simple class created for tracking the existence of a
       * more chip across activity restarts/
       */
      private class MoreImageSpan extends ReplacementDrawableSpan {
          public MoreImageSpan(Drawable b) {
              super(b);
              setExtraMargin(mLineSpacingExtra);
          }
      }
  
      @Override
      public boolean onDown(MotionEvent e) {
          return false;
      }
  
      @Override
      public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
          // Do nothing.
          return false;
      }
  
      @Override
      public void onLongPress(MotionEvent event) {
          if (mSelectedChip != null) {
              return;
          }
          float x = event.getX();
          float y = event.getY();
          final int offset = putOffsetInRange(x, y);
          DrawableRecipientChip currentChip = findChip(offset);
          if (currentChip != null) {
              if (mDragEnabled) {
                  // Start drag-and-drop for the selected chip.
                  startDrag(currentChip);
              } else {
                  // Copy the selected chip email address.
                  showCopyDialog(currentChip.getEntry().getDestination());
              }
          }
      }
  
      // 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(float x, float y) {
          if (getLayout() == null) return -1;
          final int line = supportGetLineAtCoordinate(y);
          return supportGetOffsetAtCoordinate(line, x);
      }
  
      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(int line, float x) {
          x = supportConvertToLocalHorizontalCoordinate(x);
          return getLayout().getOffsetForHorizontal(line, x);
      }
      /////////////////////////////////////////////////
  
      /**
       * Enables drag-and-drop for chips.
       */
      public void enableDrag() {
          mDragEnabled = true;
      }
  
      /**
       * Starts drag-and-drop for the selected chip.
       */
      private void startDrag(DrawableRecipientChip currentChip) {
          String address = currentChip.getEntry().getDestination();
          ClipData data = ClipData.newPlainText(address, address + COMMIT_CHAR_COMMA);
  
          // Start drag mode.
          startDrag(data, new RecipientChipShadow(currentChip), null, 0);
  
          // Remove the current chip, so drag-and-drop will result in a move.
          // TODO (phamm): consider readd this chip if it's dropped outside a target.
          removeChip(currentChip);
      }
  
      /**
       * Handles drag event.
       */
      @Override
      public boolean onDragEvent(@NonNull DragEvent event) {
          switch (event.getAction()) {
              case DragEvent.ACTION_DRAG_STARTED:
                  // Only handle plain text drag and drop.
                  return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
              case DragEvent.ACTION_DRAG_ENTERED:
                  requestFocus();
                  return true;
              case DragEvent.ACTION_DROP:
                  handlePasteClip(event.getClipData());
                  return true;
          }
          return false;
      }
  
      /**
       * Drag shadow for a {@link DrawableRecipientChip}.
       */
      private final class RecipientChipShadow extends DragShadowBuilder {
          private final DrawableRecipientChip mChip;
  
          public RecipientChipShadow(DrawableRecipientChip chip) {
              mChip = chip;
          }
  
          @Override
          public void onProvideShadowMetrics(@NonNull Point shadowSize,
                  @NonNull Point shadowTouchPoint) {
              Rect rect = mChip.getBounds();
              shadowSize.set(rect.width(), rect.height());
              shadowTouchPoint.set(rect.centerX(), rect.centerY());
          }
  
          @Override
          public void onDrawShadow(@NonNull Canvas canvas) {
              mChip.draw(canvas);
          }
      }
  
      private void showCopyDialog(final String address) {
          final Context context = getContext();
          if (!mAttachedToWindow || context == null || !(context instanceof Activity)) {
              return;
          }
  
          final DialogFragment fragment = CopyDialog.newInstance(address);
          fragment.show(((Activity) context).getFragmentManager(), CopyDialog.TAG);
      }
  
      @Override
      public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
          // Do nothing.
          return false;
      }
  
      @Override
      public void onShowPress(MotionEvent e) {
          // Do nothing.
      }
  
      @Override
      public boolean onSingleTapUp(MotionEvent e) {
          // Do nothing.
          return false;
      }
  
      protected boolean isPhoneQuery() {
          return getAdapter() != null
                  && getAdapter().getQueryType() == BaseRecipientAdapter.QUERY_TYPE_PHONE;
      }
  
      @Override
      public BaseRecipientAdapter getAdapter() {
          return (BaseRecipientAdapter) super.getAdapter();
      }
  
      /**
       * Append a new {@link RecipientEntry} to the end of the recipient chips, leaving any
       * unfinished text at the end.
       */
      public void appendRecipientEntry(final RecipientEntry entry) {
          clearComposingText();
  
          final Editable editable = getText();
          int chipInsertionPoint = 0;
  
          // Find the end of last chip and see if there's any unchipified text.
          final DrawableRecipientChip[] recips = getSortedRecipients();
          if (recips != null && recips.length > 0) {
              final DrawableRecipientChip last = recips[recips.length - 1];
              // The chip will be inserted at the end of last chip + 1. All the unfinished text after
              // the insertion point will be kept untouched.
              chipInsertionPoint = editable.getSpanEnd(last) + 1;
          }
  
          final CharSequence chip = createChip(entry);
          if (chip != null) {
              editable.insert(chipInsertionPoint, chip);
          }
      }
  
      /**
       * Remove all chips matching the given RecipientEntry.
       */
      public void removeRecipientEntry(final RecipientEntry entry) {
          final DrawableRecipientChip[] recips = getText()
                  .getSpans(0, getText().length(), DrawableRecipientChip.class);
  
          for (final DrawableRecipientChip recipient : recips) {
              final RecipientEntry existingEntry = recipient.getEntry();
              if (existingEntry != null && existingEntry.isValid() &&
                      existingEntry.isSamePerson(entry)) {
                  removeChip(recipient);
              }
          }
      }
  
      public void setAlternatePopupAnchor(View v) {
          mAlternatePopupAnchor = v;
      }
  
      @Override
      public void setVisibility(int visibility) {
          super.setVisibility(visibility);
  
          if (visibility != GONE && mRequiresShrinkWhenNotGone) {
              mRequiresShrinkWhenNotGone = false;
              mHandler.post(mDelayedShrink);
          }
      }
  
      private static class ChipBitmapContainer {
          Bitmap bitmap;
          // information used for positioning the loaded icon
          boolean loadIcon = true;
          float left;
          float top;
          float right;
          float bottom;
          // information used for positioning the warning icon
          float warningIconLeft;
          float warningIconTop;
          float warningIconRight;
          float warningIconBottom;
      }
  }
  