RecipientEditTextView.java revision 6f673fc74c7ca3c964cd384bd596ae031dbd3e1c
1b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner/*
2b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner
3b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * Copyright (C) 2011 The Android Open Source Project
4b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner *
5b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * Licensed under the Apache License, Version 2.0 (the "License");
6b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * you may not use this file except in compliance with the License.
7b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * You may obtain a copy of the License at
8b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner *
9b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner *      http://www.apache.org/licenses/LICENSE-2.0
10b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner *
11b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * Unless required by applicable law or agreed to in writing, software
12b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * distributed under the License is distributed on an "AS IS" BASIS,
13b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * See the License for the specific language governing permissions and
15b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner * limitations under the License.
16b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner */
17b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner
18b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautnerpackage com.android.ex.chips;
19d53f09254ed48365d3a5149d640437d76aed2e5dJorim Jaggi
20031384556ede1678e9c7f5bff21a6b9eefb3502fRobert Carrimport android.app.Dialog;
213797c22ea16e932329ebffdc7e7ce09f9ecd9545Wale Ogunwaleimport android.content.ClipData;
223797c22ea16e932329ebffdc7e7ce09f9ecd9545Wale Ogunwaleimport android.content.ClipDescription;
230bd180d8880b3d1b9677f154c034a2af840b4796Filip Gruszczynskiimport android.content.ClipboardManager;
24d53f09254ed48365d3a5149d640437d76aed2e5dJorim Jaggiimport android.content.Context;
25d53f09254ed48365d3a5149d640437d76aed2e5dJorim Jaggiimport android.content.DialogInterface;
261ed0d89e7e9a28a5dd52fdc40425466efd8d08efWale Ogunwaleimport android.content.DialogInterface.OnDismissListener;
2799db1863a84364339fc5dc9142f15910cdd96ed8Wale Ogunwaleimport android.content.res.Resources;
283797c22ea16e932329ebffdc7e7ce09f9ecd9545Wale Ogunwaleimport android.content.res.TypedArray;
29b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwaleimport android.graphics.Bitmap;
30e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.graphics.BitmapFactory;
31e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.graphics.BitmapShader;
322c2549c5f44b712dbbf66a69d91f07d6f5336ee6Craig Mautnerimport android.graphics.Canvas;
3342bf39edbdad19f51497938d0a3469dd772f19e8Craig Mautnerimport android.graphics.Color;
34e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.graphics.Matrix;
35e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.graphics.Paint;
36e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.graphics.Paint.Style;
37e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautnerimport android.graphics.Point;
382c2549c5f44b712dbbf66a69d91f07d6f5336ee6Craig Mautnerimport android.graphics.Rect;
39e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.graphics.RectF;
40e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.graphics.Shader.TileMode;
41f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwaleimport android.graphics.drawable.BitmapDrawable;
42b9b16a74e5543b7b707e55a7382bbe82d300e2e5Wale Ogunwaleimport android.graphics.drawable.Drawable;
432cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwaleimport android.graphics.drawable.StateListDrawable;
443eadad75664e71519deebcf7a978b492952616ffWale Ogunwaleimport android.os.AsyncTask;
452cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwaleimport android.os.Build;
463eadad75664e71519deebcf7a978b492952616ffWale Ogunwaleimport android.os.Handler;
472cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwaleimport android.os.Looper;
483eadad75664e71519deebcf7a978b492952616ffWale Ogunwaleimport android.os.Message;
492cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwaleimport android.os.Parcelable;
50f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwaleimport android.text.Editable;
51c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautnerimport android.text.InputType;
5283162a90278d9d52d8fca7ee20ba314b452261deCraig Mautnerimport android.text.Layout;
53ac6f843c917b68ea8805711965b149a9338e3a0eCraig Mautnerimport android.text.Spannable;
543eadad75664e71519deebcf7a978b492952616ffWale Ogunwaleimport android.text.SpannableString;
55e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautnerimport android.text.SpannableStringBuilder;
56b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautnerimport android.text.Spanned;
57e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.text.TextPaint;
58e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.text.TextUtils;
590429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggiimport android.text.TextWatcher;
6026c8c42bbb2b998e609983886fad5968f033268dJorim Jaggiimport android.text.method.QwertyKeyListener;
61e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.text.util.Rfc822Token;
62dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggiimport android.text.util.Rfc822Tokenizer;
63dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggiimport android.util.AttributeSet;
64dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggiimport android.util.Log;
65e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.ActionMode;
663eadad75664e71519deebcf7a978b492952616ffWale Ogunwaleimport android.view.ActionMode.Callback;
67e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.DragEvent;
68e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.GestureDetector;
695a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwaleimport android.view.KeyEvent;
70e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.LayoutInflater;
71e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.Menu;
72e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.MenuItem;
73e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.MotionEvent;
74e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.View;
75e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwaleimport android.view.View.OnClickListener;
76b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwaleimport android.view.ViewParent;
77b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwaleimport android.view.accessibility.AccessibilityEvent;
78b15758ab7a6481717d0d29612e870d7241061c31Chong Zhangimport android.view.accessibility.AccessibilityManager;
793005e75585dcda30b64603e320e0711b583624ddChong Zhangimport android.view.inputmethod.EditorInfo;
803005e75585dcda30b64603e320e0711b583624ddChong Zhangimport android.view.inputmethod.InputConnection;
810b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggiimport android.widget.AdapterView;
823005e75585dcda30b64603e320e0711b583624ddChong Zhangimport android.widget.AdapterView.OnItemClickListener;
83b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwaleimport android.widget.Button;
84b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwaleimport android.widget.Filterable;
85dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan Heimport android.widget.ListAdapter;
86dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan Heimport android.widget.ListPopupWindow;
87dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan Heimport android.widget.ListView;
88ebcc875f10f05db7365cd8afbf4e9425221ab14dFilip Gruszczynskiimport android.widget.MultiAutoCompleteTextView;
898072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulianimport android.widget.ScrollView;
9083162a90278d9d52d8fca7ee20ba314b452261deCraig Mautnerimport android.widget.TextView;
91c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautner
92ac6f843c917b68ea8805711965b149a9338e3a0eCraig Mautnerimport com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
93e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautnerimport com.android.ex.chips.recipientchip.DrawableRecipientChip;
94dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan Heimport com.android.ex.chips.recipientchip.InvisibleRecipientChip;
958072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulianimport com.android.ex.chips.recipientchip.ReplacementDrawableSpan;
96c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautnerimport com.android.ex.chips.recipientchip.VisibleRecipientChip;
97c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautner
98c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautnerimport java.util.ArrayList;
99c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautnerimport java.util.Arrays;
100c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautnerimport java.util.Collections;
101c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautnerimport java.util.Comparator;
102b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwaleimport java.util.List;
103f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwaleimport java.util.Map;
10483162a90278d9d52d8fca7ee20ba314b452261deCraig Mautnerimport java.util.Set;
10583162a90278d9d52d8fca7ee20ba314b452261deCraig Mautnerimport java.util.regex.Matcher;
10683162a90278d9d52d8fca7ee20ba314b452261deCraig Mautnerimport java.util.regex.Pattern;
10783162a90278d9d52d8fca7ee20ba314b452261deCraig Mautner
108f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale/**
10983162a90278d9d52d8fca7ee20ba314b452261deCraig Mautner * RecipientEditTextView is an auto complete text view for use with applications
11083162a90278d9d52d8fca7ee20ba314b452261deCraig Mautner * that use the new Chips UI for addressing a message to recipients.
11183162a90278d9d52d8fca7ee20ba314b452261deCraig Mautner */
11201f79cf91610ec9f85345ea6eeae50ea2f28578fCraig Mautnerpublic class RecipientEditTextView extends MultiAutoCompleteTextView implements
11342bf39edbdad19f51497938d0a3469dd772f19e8Craig Mautner        OnItemClickListener, Callback, RecipientAlternatesAdapter.OnCheckedItemChangedListener,
114f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale        GestureDetector.OnGestureListener, OnDismissListener, OnClickListener,
115441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian        TextView.OnEditorActionListener, DropdownChipLayouter.ChipDeleteListener {
116441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian    private static final String TAG = "RecipientEditTextView";
117441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian
118f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private static final char COMMIT_CHAR_COMMA = ',';
119f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private static final char COMMIT_CHAR_SEMICOLON = ';';
12083162a90278d9d52d8fca7ee20ba314b452261deCraig Mautner    private static final char COMMIT_CHAR_SPACE = ' ';
12142bf39edbdad19f51497938d0a3469dd772f19e8Craig Mautner    private static final String SEPARATOR = String.valueOf(COMMIT_CHAR_COMMA)
122b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale            + String.valueOf(COMMIT_CHAR_SPACE);
123b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale
124c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautner    // This pattern comes from android.util.Patterns. It has been tweaked to handle a "1" before
125c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautner    // parens, so numbers such as "1 (425) 222-2342" match.
126e42d0e102dbdf5287703389183a69019b64fc35eWale Ogunwale    private static final Pattern PHONE_PATTERN
127f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale            = Pattern.compile(                                  // sdd = space, dot, or dash
128f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale            "(\\+[0-9]+[\\- \\.]*)?"                    // +<digits><sdd>*
1297e8eeb79abe1a4b39b2f086aacefa48928307cb2Chong Zhang                    + "(1?[ ]*\\([0-9]+\\)[\\- \\.]*)?"         // 1(<digits>)<sdd>*
1307e8eeb79abe1a4b39b2f086aacefa48928307cb2Chong Zhang                    + "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit>
1317e8eeb79abe1a4b39b2f086aacefa48928307cb2Chong Zhang
1327e8eeb79abe1a4b39b2f086aacefa48928307cb2Chong Zhang    private static final int DISMISS = "dismiss".hashCode();
1337e8eeb79abe1a4b39b2f086aacefa48928307cb2Chong Zhang    private static final long DISMISS_DELAY = 300;
1347e8eeb79abe1a4b39b2f086aacefa48928307cb2Chong Zhang
135f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    // TODO: get correct number/ algorithm from with UX.
136f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    // Visible for testing.
137e42d0e102dbdf5287703389183a69019b64fc35eWale Ogunwale    /*package*/ static final int CHIP_LIMIT = 2;
138b9b16a74e5543b7b707e55a7382bbe82d300e2e5Wale Ogunwale
139e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner    private static final int MAX_CHIPS_PARSED = 50;
140e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner
141e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner    private static int sSelectedTextColor = -1;
142b9b16a74e5543b7b707e55a7382bbe82d300e2e5Wale Ogunwale
14383162a90278d9d52d8fca7ee20ba314b452261deCraig Mautner    // Work variables to avoid re-allocation on every typed character.
144e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner    private final Rect mRect = new Rect();
1450689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski    private final int[] mCoords = new int[2];
1460689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski
147112eb8c1f76fff6a670d7c5f85e8c3d656cd3aa8Chong Zhang    // Resources for displaying chips.
1480689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski    private Drawable mChipBackground = null;
149441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian    private Drawable mChipDelete = null;
15083162a90278d9d52d8fca7ee20ba314b452261deCraig Mautner    private Drawable mInvalidChipBackground;
151e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner    private Drawable mChipBackgroundPressed;
152e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner
153f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    // Possible attr overrides
15453a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale    private float mChipHeight;
15553a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale    private float mChipFontSize;
15653a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale    private float mLineSpacingExtra;
15753a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale    private int mChipTextStartPadding;
158b9b16a74e5543b7b707e55a7382bbe82d300e2e5Wale Ogunwale    private int mChipTextEndPadding;
15953a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale    private final int mTextHeight;
160000957cef387dc7d08fc6563e2221e9023194984Wale Ogunwale    private boolean mDisableDelete;
161441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian    private int mMaxLines;
16253a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale
16353a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale    /**
16453a29a90f35f72462c0d6ad650921d5566c1f8f0Wale Ogunwale     * Enumerator for avatar position. See attr.xml for more details.
1651779e6108ab264689b7d5e5c42ba3cbca6c8189fAndrii Kulian     * 0 for end, 1 for start.
1661779e6108ab264689b7d5e5c42ba3cbca6c8189fAndrii Kulian     */
167ddc1cb2c15549ed23dce9d416680a009fa6ae23cWale Ogunwale    private int mAvatarPosition;
168b9b16a74e5543b7b707e55a7382bbe82d300e2e5Wale Ogunwale    private static final int AVATAR_POSITION_END = 0;
169ddc1cb2c15549ed23dce9d416680a009fa6ae23cWale Ogunwale    private static final int AVATAR_POSITION_START = 1;
170ddc1cb2c15549ed23dce9d416680a009fa6ae23cWale Ogunwale
17114a3fb98e5ec5f366e810ad4d1f2a73ef8407ac6Wale Ogunwale    private Paint mWorkPaint = new Paint();
172ddc1cb2c15549ed23dce9d416680a009fa6ae23cWale Ogunwale
173ddc1cb2c15549ed23dce9d416680a009fa6ae23cWale Ogunwale    private Tokenizer mTokenizer;
1741779e6108ab264689b7d5e5c42ba3cbca6c8189fAndrii Kulian    private Validator mValidator;
1755e6968db630d26872306c4b23aa2c600d83ed454Jorim Jaggi    private Handler mHandler;
176f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private TextWatcher mTextWatcher;
177f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private DropdownChipLayouter mDropdownChipLayouter;
1785e6968db630d26872306c4b23aa2c600d83ed454Jorim Jaggi
179ddc1cb2c15549ed23dce9d416680a009fa6ae23cWale Ogunwale    private View mDropdownAnchor = this;
180ddc1cb2c15549ed23dce9d416680a009fa6ae23cWale Ogunwale    private ListPopupWindow mAlternatesPopup;
18114a3fb98e5ec5f366e810ad4d1f2a73ef8407ac6Wale Ogunwale    private ListPopupWindow mAddressPopup;
182f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private View mAlternatePopupAnchor;
183f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private OnItemClickListener mAlternatesListener;
184f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale
185f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private DrawableRecipientChip mSelectedChip;
1863f4433dadb7627153939ebb7d88d080c132b7f7fWale Ogunwale    private Bitmap mDefaultContactPhoto;
1873f4433dadb7627153939ebb7d88d080c132b7f7fWale Ogunwale    private ReplacementDrawableSpan mMoreChip;
188f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private TextView mMoreItem;
189571771c3fc912e63f83b75693c0f3c85ec9622daWale Ogunwale
190f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private boolean mIsAccessibilityOn;
1910429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi    private int mCurrentSuggestionCount;
192e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner
193f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    // VisibleForTesting
194e3119b7d353e71d1f94ddff932b722b4d285931eCraig Mautner    final ArrayList<String> mPendingChips = new ArrayList<String>();
195c00204b4d14d49a0417b44ca21aee4f0d4c466e0Craig Mautner
196f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    private int mPendingChipsCount = 0;
197b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner    private int mCheckedItem;
1985d9c7be84d9628c1cf199fcf9015942835c4671bCraig Mautner    private boolean mNoChips = false;
199cbd84af39a329890013b0c3b6763280ba2ad78c9Craig Mautner    private boolean mShouldShrink = true;
200f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale
201f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    // VisibleForTesting
202cbd84af39a329890013b0c3b6763280ba2ad78c9Craig Mautner    ArrayList<DrawableRecipientChip> mTemporaryRecipients;
203cbd84af39a329890013b0c3b6763280ba2ad78c9Craig Mautner
204cbd84af39a329890013b0c3b6763280ba2ad78c9Craig Mautner    private ArrayList<DrawableRecipientChip> mRemovedSpans;
205ebcc875f10f05db7365cd8afbf4e9425221ab14dFilip Gruszczynski
2068072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian    // Chip copy fields.
2078072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian    private GestureDetector mGestureDetector;
2088072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian    private Dialog mCopyDialog;
209b34a7ad1af54132b6b046ab8f768e0ffb81cf581Wale Ogunwale    private String mCopyAddress;
2108072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian
211ebcc875f10f05db7365cd8afbf4e9425221ab14dFilip Gruszczynski    // Obtain the enclosing scroll view, if it exists, so that the view can be
2128072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian    // scrolled to show the last line of chips content.
213b34a7ad1af54132b6b046ab8f768e0ffb81cf581Wale Ogunwale    private ScrollView mScrollView;
2148072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian    private boolean mTriedGettingScrollView;
215ebcc875f10f05db7365cd8afbf4e9425221ab14dFilip Gruszczynski    private boolean mDragEnabled = false;
216b34a7ad1af54132b6b046ab8f768e0ffb81cf581Wale Ogunwale
2175a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale    private boolean mAttachedToWindow;
218e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale
219e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    private final Runnable mAddTextWatcher = new Runnable() {
220e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        @Override
221e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        public void run() {
222e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            if (mTextWatcher == null) {
2235a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale                mTextWatcher = new RecipientTextWatcher();
2245a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale                addTextChangedListener(mTextWatcher);
225e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            }
226e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        }
227e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    };
228e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale
229e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    private IndividualReplacementTask mIndividualReplacements;
230e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale
2312cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale    private Runnable mHandlePendingChips = new Runnable() {
232e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale
2335a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale        @Override
2342cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale        public void run() {
2352cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale            handlePendingChips();
2362cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale        }
2372cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale
2382e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang    };
2392cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale
2402cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale    private Runnable mDelayedShrink = new Runnable() {
2412e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang
2422cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale        @Override
243e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        public void run() {
244e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            shrink();
2452e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang        }
246f66db43063e4409206f1572fa1bac4f87c431f9aChong Zhang
247e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    };
2480689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski
249112eb8c1f76fff6a670d7c5f85e8c3d656cd3aa8Chong Zhang    private RecipientEntryItemClickedListener mRecipientEntryItemClickedListener;
2500689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski
251441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian    public interface RecipientEntryItemClickedListener {
2522cc92f55c0257cdc837585b36987c610fb0a8251Wale Ogunwale        /**
253e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale         * Callback that occurs whenever an auto-complete suggestion is clicked.
254e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale         * @param charactersTyped the number of characters typed by the user to provide the
255dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi         *                        auto-complete suggestions.
256dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi         * @param position the position in the dropdown list that the user clicked
257dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi         */
258dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        void onRecipientEntryItemClicked(int charactersTyped, int position);
259dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi    }
260dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi
261dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi    public RecipientEditTextView(Context context, AttributeSet attrs) {
262dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        super(context, attrs);
263dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        setChipDimensions(context, attrs);
264dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        mTextHeight = calculateTextHeight();
265dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        if (sSelectedTextColor == -1) {
266dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi            sSelectedTextColor = context.getResources().getColor(android.R.color.white);
267dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        }
268dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        mAlternatesPopup = new ListPopupWindow(context);
269dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        mAlternatesPopup.setBackgroundDrawable(null);
270dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        mAddressPopup = new ListPopupWindow(context);
271dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        mAddressPopup.setBackgroundDrawable(null);
272dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        mCopyDialog = new Dialog(context);
273dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi        mAlternatesListener = new OnItemClickListener() {
274dc249c4ae7c7838928f53f151bfda8a6817b8784Jorim Jaggi            @Override
275b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale            public void onItemClick(AdapterView<?> adapterView,View view, int position,
276b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale                    long rowId) {
277b15758ab7a6481717d0d29612e870d7241061c31Chong Zhang                mAlternatesPopup.setOnItemClickListener(null);
278b15758ab7a6481717d0d29612e870d7241061c31Chong Zhang                replaceChip(mSelectedChip, ((RecipientAlternatesAdapter) adapterView.getAdapter())
279b15758ab7a6481717d0d29612e870d7241061c31Chong Zhang                        .getRecipientEntry(position));
280df241e97715abf10fc043233d18ac4cdd5c0f889Wale Ogunwale                Message delayed = Message.obtain(mHandler, DISMISS);
281b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale                delayed.obj = mAlternatesPopup;
282b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale                mHandler.sendMessageDelayed(delayed, DISMISS_DELAY);
283dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan He                clearComposingText();
284dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan He            }
285dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan He        };
286dd1e66f737271dedded652cfae69a0cd7d46a86cJiaquan He        setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
287b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale        setOnItemClickListener(this);
288df241e97715abf10fc043233d18ac4cdd5c0f889Wale Ogunwale        setCustomSelectionActionModeCallback(this);
289b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale        mHandler = new Handler() {
290b1faf60b896afe235175354ffd90290ff93a54b4Wale Ogunwale            @Override
29142625d1bc7ef99c4d4435e8cdebfe3eee57b8d97Jorim Jaggi            public void handleMessage(Message msg) {
29242625d1bc7ef99c4d4435e8cdebfe3eee57b8d97Jorim Jaggi                if (msg.what == DISMISS) {
29342625d1bc7ef99c4d4435e8cdebfe3eee57b8d97Jorim Jaggi                    ((ListPopupWindow) msg.obj).dismiss();
29442625d1bc7ef99c4d4435e8cdebfe3eee57b8d97Jorim Jaggi                    return;
2958072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian                }
2968072d11f6a41a68600a15623ca5316ca0def1856Andrii Kulian                super.handleMessage(msg);
29787b21722c2336490ecf8b66f6acfc46ce8cc6f46Chong Zhang            }
2983005e75585dcda30b64603e320e0711b583624ddChong Zhang        };
2993005e75585dcda30b64603e320e0711b583624ddChong Zhang        mTextWatcher = new RecipientTextWatcher();
3003005e75585dcda30b64603e320e0711b583624ddChong Zhang        addTextChangedListener(mTextWatcher);
3013005e75585dcda30b64603e320e0711b583624ddChong Zhang        mGestureDetector = new GestureDetector(context, this);
3023005e75585dcda30b64603e320e0711b583624ddChong Zhang        setOnEditorActionListener(this);
3033005e75585dcda30b64603e320e0711b583624ddChong Zhang
304d1c3791659572529dbc749b70d1c33bda1bdbc32Wale Ogunwale        setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context));
305bd0d937303ae54d8a5bb5f08080c4164302daefcChong Zhang    }
306d1c3791659572529dbc749b70d1c33bda1bdbc32Wale Ogunwale
3073005e75585dcda30b64603e320e0711b583624ddChong Zhang    @Override
3083005e75585dcda30b64603e320e0711b583624ddChong Zhang    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
3093005e75585dcda30b64603e320e0711b583624ddChong Zhang        super.onLayout(changed, left, top, right, bottom);
3103005e75585dcda30b64603e320e0711b583624ddChong Zhang
3110429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi        final AccessibilityManager accessibilityManager =
3120429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi                (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
3130429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi        mIsAccessibilityOn = accessibilityManager.isEnabled();
3140429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi    }
3150429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi
3160429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi    private int calculateTextHeight() {
317441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian        final TextPaint paint = getPaint();
3180429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi
3190429f3522bca5bb86378dd3f013f995484ddbed6Jorim Jaggi        mRect.setEmpty();
3205117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        // First measure the bounds of a sample text.
3215117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        final String textHeightSample = "a";
3225117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        paint.getTextBounds(textHeightSample, 0, textHeightSample.length(), mRect);
3235117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang
3245117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        mRect.left = 0;
3255117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        mRect.right = 0;
3265117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang
3275117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        return mRect.height();
3285117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    }
329441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian
3305406e7ade87c33f70c83a283781dcc48fb67cdb9Andrii Kulian    public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) {
3315406e7ade87c33f70c83a283781dcc48fb67cdb9Andrii Kulian        mDropdownChipLayouter = dropdownChipLayouter;
3325406e7ade87c33f70c83a283781dcc48fb67cdb9Andrii Kulian        mDropdownChipLayouter.setDeleteListener(this);
3335117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    }
3345117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang
3355117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    public void setRecipientEntryItemClickedListener(RecipientEntryItemClickedListener listener) {
3365117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        mRecipientEntryItemClickedListener = listener;
3375117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    }
3385117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang
3395117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    @Override
3405117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    protected void onDetachedFromWindow() {
3415117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        super.onDetachedFromWindow();
3425117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang        mAttachedToWindow = false;
3435117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    }
3445406e7ade87c33f70c83a283781dcc48fb67cdb9Andrii Kulian
3455117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    @Override
3465117e273eca0b15ef057d0c2440546799c17edf4Chong Zhang    protected void onAttachedToWindow() {
347f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale        super.onAttachedToWindow();
348f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale        mAttachedToWindow = true;
349f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale
3505a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale        final int anchorId = getDropDownAnchor();
3513797c22ea16e932329ebffdc7e7ce09f9ecd9545Wale Ogunwale        if (anchorId != View.NO_ID) {
352f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale            mDropdownAnchor = getRootView().findViewById(anchorId);
353f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale        }
354f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale    }
355f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale
3564c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    @Override
357ccb6ce2dcd8b833692f6c8a4a568fb938acfe058Wale Ogunwale    public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
358f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale        if (action == EditorInfo.IME_ACTION_DONE) {
359f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale            if (commitDefault()) {
360f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale                return true;
361f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale            }
362f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale            if (mSelectedChip != null) {
363f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale                clearSelectedChip();
364f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale                return true;
365ccb6ce2dcd8b833692f6c8a4a568fb938acfe058Wale Ogunwale            } else if (focusNext()) {
366ccb6ce2dcd8b833692f6c8a4a568fb938acfe058Wale Ogunwale                return true;
367f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale            }
368e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        }
369e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        return false;
3704c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    }
3714c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang
3724c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    @Override
3734c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
3744c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        InputConnection connection = super.onCreateInputConnection(outAttrs);
3754c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        int imeActions = outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION;
3764c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        if ((imeActions&EditorInfo.IME_ACTION_DONE) != 0) {
3774c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            // clear the existing action
3784c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            outAttrs.imeOptions ^= imeActions;
3794c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            // set the DONE action
3804c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
3814c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        }
3824c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
3834c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
384f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale        }
385f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale
386d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang        outAttrs.actionId = EditorInfo.IME_ACTION_DONE;
387d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang
388d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang        // Custom action labels are discouraged in L; a checkmark icon is shown in place of the
389d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang        // custom text in this case.
390d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang        outAttrs.actionLabel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? null :
3914c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            getContext().getString(R.string.action_label);
3924c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        return connection;
3934c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    }
3944c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang
3954c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    /*package*/ DrawableRecipientChip getLastChip() {
3964c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        DrawableRecipientChip last = null;
3974c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        DrawableRecipientChip[] chips = getSortedRecipients();
3984c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        if (chips != null && chips.length > 0) {
3994c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            last = chips[chips.length - 1];
4004c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        }
4014c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        return last;
4024c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    }
4034c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang
4044c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    /**
4054c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang     * @return The list of {@link RecipientEntry}s that have been selected by the user.
4064c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang     */
4074c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    public List<RecipientEntry> getSelectedRecipients() {
4084c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        DrawableRecipientChip[] chips =
4094c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang                getText().getSpans(0, getText().length(), DrawableRecipientChip.class);
4104c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        List<RecipientEntry> results = new ArrayList();
4114c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        if (chips == null) {
4124c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            return results;
4134c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        }
4144c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang
4154c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        for (DrawableRecipientChip c : chips) {
4164c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang            results.add(c.getEntry());
4174c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        }
418a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr
419a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr        return results;
420a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr    }
421f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale
422f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    @Override
4234c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang    public void onSelectionChanged(int start, int end) {
4244c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        // When selection changes, see if it is inside the chips area.
4254c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        // If so, move the cursor back after the chips again.
4264c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        DrawableRecipientChip last = getLastChip();
4274c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        if (last != null && start < getSpannable().getSpanEnd(last)) {
4285a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale            // Grab the last chip and set the cursor to after it.
42922a869f51b93242d001ffb4c5d4ab3c8ed0b5a16Jorim Jaggi            setSelection(Math.min(getSpannable().getSpanEnd(last) + 1, getText().length()));
43022a869f51b93242d001ffb4c5d4ab3c8ed0b5a16Jorim Jaggi        }
431a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr        super.onSelectionChanged(start, end);
432a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr    }
433a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr
434a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr    @Override
435a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr    public void onRestoreInstanceState(Parcelable state) {
436a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr        if (!TextUtils.isEmpty(getText())) {
437a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr            super.onRestoreInstanceState(null);
438a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr        } else {
439a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr            super.onRestoreInstanceState(state);
440a86a6bf73e8f6e58590c1b53972cd2d1cc7c137fRobert Carr        }
44122a869f51b93242d001ffb4c5d4ab3c8ed0b5a16Jorim Jaggi    }
44222a869f51b93242d001ffb4c5d4ab3c8ed0b5a16Jorim Jaggi
44322a869f51b93242d001ffb4c5d4ab3c8ed0b5a16Jorim Jaggi    @Override
44422a869f51b93242d001ffb4c5d4ab3c8ed0b5a16Jorim Jaggi    public Parcelable onSaveInstanceState() {
4454c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        // If the user changes orientation while they are editing, just roll back the selection.
4464c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        clearSelectedChip();
4474c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang        return super.onSaveInstanceState();
448f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    }
449f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale
450f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    /**
451f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale     * Convenience method: Append the specified text slice to the TextView's
452f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale     * display buffer, upgrading it to BufferType.EDITABLE if it was
4534c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang     * not already editable. Commas are excluded as they are added automatically
4544c9ba52acad52f1c1087415e04b2a17614707e40Chong Zhang     * by the view.
4550b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi     */
456c662d8e94620c84b2fa57bfd6e45690f0c349d89Jorim Jaggi    @Override
4570b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi    public void append(CharSequence text, int start, int end) {
4580b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi        // We don't care about watching text changes while appending.
4590b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi        if (mTextWatcher != null) {
4600b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi            removeTextChangedListener(mTextWatcher);
461c662d8e94620c84b2fa57bfd6e45690f0c349d89Jorim Jaggi        }
4620b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi        super.append(text, start, end);
463c662d8e94620c84b2fa57bfd6e45690f0c349d89Jorim Jaggi        if (!TextUtils.isEmpty(text) && TextUtils.getTrimmedLength(text) > 0) {
464c662d8e94620c84b2fa57bfd6e45690f0c349d89Jorim Jaggi            String displayString = text.toString();
465c662d8e94620c84b2fa57bfd6e45690f0c349d89Jorim Jaggi
466c662d8e94620c84b2fa57bfd6e45690f0c349d89Jorim Jaggi            if (!displayString.trim().endsWith(String.valueOf(COMMIT_CHAR_COMMA))) {
4673005e75585dcda30b64603e320e0711b583624ddChong Zhang                // We have no separator, so we should add it
468d1c3791659572529dbc749b70d1c33bda1bdbc32Wale Ogunwale                super.append(SEPARATOR, 0, SEPARATOR.length());
4693005e75585dcda30b64603e320e0711b583624ddChong Zhang                displayString += SEPARATOR;
4703005e75585dcda30b64603e320e0711b583624ddChong Zhang            }
4710b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi
4720b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi            if (!TextUtils.isEmpty(displayString)
4730b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi                    && TextUtils.getTrimmedLength(displayString) > 0) {
4740b46f3c72c324bc9b8890ed9b81951bbeec70fddJorim Jaggi                mPendingChipsCount++;
475e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale                mPendingChips.add(displayString);
476e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            }
477e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        }
478e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        // Put a message on the queue to make sure we ALWAYS handle pending
4795a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale        // chips.
480ebcc875f10f05db7365cd8afbf4e9425221ab14dFilip Gruszczynski        if (mPendingChipsCount > 0) {
481e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            postHandlePendingChips();
482e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        }
483e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        mHandler.post(mAddTextWatcher);
484e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    }
485e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale
486e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    @Override
487e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    public void onFocusChanged(boolean hasFocus, int direction, Rect previous) {
488e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale        super.onFocusChanged(hasFocus, direction, previous);
4892e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang        if (!hasFocus) {
4902e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang            shrink();
491e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale        } else {
4922e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang            expand();
4932e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang        }
494e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale    }
495e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale
496441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian    @Override
497e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale    public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
498e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale        super.setAdapter(adapter);
499e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale        BaseRecipientAdapter baseAdapter = (BaseRecipientAdapter) adapter;
5009474421a56c8bf66086a9d253013687eb5207331Wale Ogunwale        baseAdapter.registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
501441e4494682144aec2ec7f19060464af3d29c319Andrii Kulian            @Override
502e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale            public void onChanged(List<RecipientEntry> entries) {
503e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale                // Scroll the chips field to the top of the screen so
504e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale                // that the user can see as many results as possible.
505e1fe4d1e12b2524807ed29990b11268a03de54d7Wale Ogunwale                if (entries != null && entries.size() > 0) {
5062e2c81a8d67f6ceccf178c5c7264d27e8c72e9ffChong Zhang                    scrollBottomIntoView();
5071ed0d89e7e9a28a5dd52fdc40425466efd8d08efWale Ogunwale                    // Here the current suggestion count is still the old one since we update
508e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale                    // the count at the bottom of this function.
509e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale                    if (mCurrentSuggestionCount == 0) {
510f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                        // Announce the new number of possible choices for accessibility.
511c28098f69b8ba5d3039ecd0fa154e880f4613843Winson                        announceForAccessibilityCompat(getContext().getString(
512f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                                R.string.accessbility_suggestion_dropdown_opened));
513f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                    }
514c28098f69b8ba5d3039ecd0fa154e880f4613843Winson                }
515c28098f69b8ba5d3039ecd0fa154e880f4613843Winson
516c28098f69b8ba5d3039ecd0fa154e880f4613843Winson                // Set the dropdown height to be the remaining height from the anchor to the bottom.
517f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                mDropdownAnchor.getLocationInWindow(mCoords);
51813d30660ef6da2d924e4fc943ccd187767ee0cd2Winson                getWindowVisibleDisplayFrame(mRect);
519f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                setDropDownHeight(mRect.bottom - mCoords[1] - mDropdownAnchor.getHeight() -
520f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                    getDropDownVerticalOffset());
52113d30660ef6da2d924e4fc943ccd187767ee0cd2Winson
52213d30660ef6da2d924e4fc943ccd187767ee0cd2Winson                mCurrentSuggestionCount = entries == null ? 0 : entries.size();
52313d30660ef6da2d924e4fc943ccd187767ee0cd2Winson            }
5246dfdfd6741c5a3dd8d8a49ddbd6ee5dfe2fd292dWale Ogunwale        });
525f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale        baseAdapter.setDropdownChipLayouter(mDropdownChipLayouter);
526f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    }
527ff71d20ff357a33c30e1e3b474e51ad47d5c5d3aJorim Jaggi
528ff71d20ff357a33c30e1e3b474e51ad47d5c5d3aJorim Jaggi    private void announceForAccessibilityCompat(String text) {
529db20b5f7a1fdb847f2266df0fbae6046dc95c757Chong Zhang        if (mIsAccessibilityOn && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
530db20b5f7a1fdb847f2266df0fbae6046dc95c757Chong Zhang            final ViewParent parent = getParent();
531db20b5f7a1fdb847f2266df0fbae6046dc95c757Chong Zhang            if (parent != null) {
532db20b5f7a1fdb847f2266df0fbae6046dc95c757Chong Zhang                AccessibilityEvent event = AccessibilityEvent.obtain(
53309b21efb97d539543259b33e2da9d4c7a41966c8Chong Zhang                        AccessibilityEvent.TYPE_ANNOUNCEMENT);
53409b21efb97d539543259b33e2da9d4c7a41966c8Chong Zhang                onInitializeAccessibilityEvent(event);
53509b21efb97d539543259b33e2da9d4c7a41966c8Chong Zhang                event.getText().add(text);
53609b21efb97d539543259b33e2da9d4c7a41966c8Chong Zhang                event.setContentDescription(null);
537031384556ede1678e9c7f5bff21a6b9eefb3502fRobert Carr                parent.requestSendAccessibilityEvent(this, event);
538031384556ede1678e9c7f5bff21a6b9eefb3502fRobert Carr            }
539031384556ede1678e9c7f5bff21a6b9eefb3502fRobert Carr        }
540031384556ede1678e9c7f5bff21a6b9eefb3502fRobert Carr    }
541e627558feffa4ffe6435d7d13eda3d89f7c08095Robert Carr
542e627558feffa4ffe6435d7d13eda3d89f7c08095Robert Carr    protected void scrollBottomIntoView() {
543e627558feffa4ffe6435d7d13eda3d89f7c08095Robert Carr        if (mScrollView != null && mShouldShrink) {
544e627558feffa4ffe6435d7d13eda3d89f7c08095Robert Carr            getLocationInWindow(mCoords);
545d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang            // Desired position shows at least 1 line of chips below the action
546d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang            // bar. We add excess padding to make sure this is always below other
547d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang            // content.
5489184ec686072e9343c9dd73cf45324e5e89e042fChong Zhang            final int height = getHeight();
549bef461f6129044bc092f0c3693bfc122d1acb6d1Chong Zhang            final int currentPos = mCoords[1] + height;
550d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang            mScrollView.getLocationInWindow(mCoords);
551f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale            final int desiredPos = mCoords[1] + height / getLineCount();
552f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale            if (currentPos > desiredPos) {
553d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang                mScrollView.scrollBy(0, currentPos - desiredPos);
554d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang            }
555d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang        }
556d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang    }
557d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang
558d8ceb85512f8dc2dac6ef07fc72f89a75095e3d7Chong Zhang    protected ScrollView getScrollView() {
559bef461f6129044bc092f0c3693bfc122d1acb6d1Chong Zhang        return mScrollView;
5609184ec686072e9343c9dd73cf45324e5e89e042fChong Zhang    }
5615d9c7be84d9628c1cf199fcf9015942835c4671bCraig Mautner
56229bfbb878d54db14204e9b02dc17bfc6e127b6b2Wale Ogunwale    @Override
563df241e97715abf10fc043233d18ac4cdd5c0f889Wale Ogunwale    public void performValidation() {
56429bfbb878d54db14204e9b02dc17bfc6e127b6b2Wale Ogunwale        // Do nothing. Chips handles its own validation.
56529bfbb878d54db14204e9b02dc17bfc6e127b6b2Wale Ogunwale    }
56629bfbb878d54db14204e9b02dc17bfc6e127b6b2Wale Ogunwale
567f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale    private void shrink() {
5685a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale        if (mTokenizer == null) {
569f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale            return;
570f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale        }
571f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale        long contactId = mSelectedChip != null ? mSelectedChip.getEntry().getContactId() : -1;
572f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale        if (mSelectedChip != null && contactId != RecipientEntry.INVALID_CONTACT
573f175e8a6d0d3f3ce6be94bde451e6e03f67d0705Wale Ogunwale                && (!isPhoneQuery() && contactId != RecipientEntry.GENERATED_CONTACT)) {
574e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            clearSelectedChip();
575e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        } else {
576e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            if (getWidth() <= 0) {
577e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale                // We don't have the width yet which means the view hasn't been drawn yet
578e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale                // and there is no reason to attempt to commit chips yet.
579e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale                // This focus lost must be the result of an orientation change
580e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale                // or an initial rendering.
5819f25beee3a8cd6f452534006ea9068178cbb4ce1Wale Ogunwale                // Re-post the shrink for later.
58268e5c9e93a8f1542cd988ac01ba1d98381ff4893Robert Carr                mHandler.removeCallbacks(mDelayedShrink);
5839f25beee3a8cd6f452534006ea9068178cbb4ce1Wale Ogunwale                mHandler.post(mDelayedShrink);
584f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                return;
585f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale            }
5869f25beee3a8cd6f452534006ea9068178cbb4ce1Wale Ogunwale            // Reset any pending chips as they would have been handled
5879f25beee3a8cd6f452534006ea9068178cbb4ce1Wale Ogunwale            // when the field lost focus.
58868e5c9e93a8f1542cd988ac01ba1d98381ff4893Robert Carr            if (mPendingChipsCount > 0) {
5899f25beee3a8cd6f452534006ea9068178cbb4ce1Wale Ogunwale                postHandlePendingChips();
5909f25beee3a8cd6f452534006ea9068178cbb4ce1Wale Ogunwale            } else {
5919f25beee3a8cd6f452534006ea9068178cbb4ce1Wale Ogunwale                Editable editable = getText();
592ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                int end = getSelectionEnd();
5933f4433dadb7627153939ebb7d88d080c132b7f7fWale Ogunwale                int start = mTokenizer.findTokenStart(editable, end);
594f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                DrawableRecipientChip[] chips =
595f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale                        getSpannable().getSpans(start, end, DrawableRecipientChip.class);
596ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                if ((chips == null || chips.length == 0)) {
597ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    Editable text = getText();
598ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    int whatEnd = mTokenizer.findTokenEnd(text, start);
599ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    // This token was already tokenized, so skip past the ending token.
600ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    if (whatEnd < text.length() && text.charAt(whatEnd) == ',') {
601ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                        whatEnd = movePastTerminators(whatEnd);
602ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    }
603ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    // In the middle of chip; treat this as an edit
604ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    // and commit the whole token.
605ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    int selEnd = getSelectionEnd();
606ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    if (whatEnd != selEnd) {
607ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                        handleEdit(start, whatEnd);
608ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    } else {
609ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                        commitChip(start, end, editable);
610ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                    }
611ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale                }
6123f4433dadb7627153939ebb7d88d080c132b7f7fWale Ogunwale            }
613f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale            mHandler.post(mAddTextWatcher);
614f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale        }
615ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        createMoreChip();
616ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale    }
617ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale
618ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale    private void expand() {
619ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        if (mShouldShrink) {
620ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale            setMaxLines(Integer.MAX_VALUE);
621ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        }
622ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        removeMoreChip();
623ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        setCursorVisible(true);
624ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        Editable text = getText();
625ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        setSelection(text != null && text.length() > 0 ? text.length() : 0);
626ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        // If there are any temporary chips, try replacing them now that the user
627ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        // has expanded the field.
628ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0) {
629ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale            new RecipientReplacementTask().execute();
630ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale            mTemporaryRecipients = null;
631ec73115b0902f5ffe3a0d9dcdd51badc4a99bc0aWale Ogunwale        }
632f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale    }
6335136249a7147fb205e1b861c1d42a7d1f13b73ccWale Ogunwale
6345a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale    private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) {
6355136249a7147fb205e1b861c1d42a7d1f13b73ccWale Ogunwale        paint.setTextSize(mChipFontSize);
6365136249a7147fb205e1b861c1d42a7d1f13b73ccWale Ogunwale        if (maxWidth <= 0 && Log.isLoggable(TAG, Log.DEBUG)) {
637e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale            Log.d(TAG, "Max width is negative: " + maxWidth);
6385d9c7be84d9628c1cf199fcf9015942835c4671bCraig Mautner        }
639f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale        return TextUtils.ellipsize(text, paint, maxWidth,
6405d9c7be84d9628c1cf199fcf9015942835c4671bCraig Mautner                TextUtils.TruncateAt.END);
641e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale    }
6429adfe5776d42c9ddfd4394958993304c9d355229Wale Ogunwale
6439adfe5776d42c9ddfd4394958993304c9d355229Wale Ogunwale    /**
6449adfe5776d42c9ddfd4394958993304c9d355229Wale Ogunwale     * Creates a bitmap of the given contact on a selected chip.
6459adfe5776d42c9ddfd4394958993304c9d355229Wale Ogunwale     *
6460689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski     * @param contact The recipient entry to pull data from.
6470689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski     * @param paint The paint to use to draw the bitmap.
6480689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski     */
6490689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski    private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint) {
6500689ae903628f12ff073376b655b9d972533796bFilip Gruszczynski        paint.setColor(sSelectedTextColor);
651b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale        final ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint,
652b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale                mChipBackgroundPressed, getResources().getColor(R.color.chip_background_selected));
653b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale
654b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale        if (bitmapContainer.loadIcon) {
6555a2183d9855fccd6ee2a548872b8fe3f58e8164cWale Ogunwale            loadAvatarIcon(contact, bitmapContainer);
656b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale        }
657b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale
658f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale        return bitmapContainer.bitmap;
659b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale    }
660b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale
661b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale    /**
662b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale     * Creates a bitmap of the given contact on a selected chip.
663f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale     *
664f6192868fa4fcf85130ef40a90d0c96fbbca761dWale Ogunwale     * @param contact The recipient entry to pull data from.
665b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale     * @param paint The paint to use to draw the bitmap.
666b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale     */
667b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale    private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint) {
668b429e6826d394a63f81d1702efd714d640ddfb49Wale Ogunwale        paint.setColor(getContext().getResources().getColor(android.R.color.black));
669e4a0c5722b1d8db95dfc842d716452dbbf02c86dWale Ogunwale        ChipBitmapContainer bitmapContainer = createChipBitmap(contact, paint,
670b1fd65c0ff5784b90d765edb7e3c3115d767dff0Craig Mautner                getChipBackground(contact), getDefaultChipBackgroundColor(contact));
671
672        if (bitmapContainer.loadIcon) {
673            loadAvatarIcon(contact, bitmapContainer);
674        }
675        return bitmapContainer.bitmap;
676    }
677
678    private ChipBitmapContainer createChipBitmap(RecipientEntry contact, TextPaint paint,
679            Drawable overrideBackgroundDrawable, int backgroundColor) {
680        final ChipBitmapContainer result = new ChipBitmapContainer();
681
682        Rect backgroundPadding = new Rect();
683        if (overrideBackgroundDrawable != null) {
684            overrideBackgroundDrawable.getPadding(backgroundPadding);
685        }
686
687        // Ellipsize the text so that it takes AT MOST the entire width of the
688        // autocomplete text entry area. Make sure to leave space for padding
689        // on the sides.
690        int height = (int) mChipHeight;
691        // Since the icon is a square, it's width is equal to the maximum height it can be inside
692        // the chip. Don't include iconWidth for invalid contacts.
693        int iconWidth = contact.isValid() ?
694                height - backgroundPadding.top - backgroundPadding.bottom : 0;
695        float[] widths = new float[1];
696        paint.getTextWidths(" ", widths);
697        CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint,
698                calculateAvailableWidth() - iconWidth - widths[0] - backgroundPadding.left
699                - backgroundPadding.right);
700        int textWidth = (int) paint.measureText(ellipsizedText, 0, ellipsizedText.length());
701
702        // Chip start padding is the same as the end padding if there is no contact image.
703        final int startPadding = contact.isValid() ? mChipTextStartPadding : mChipTextEndPadding;
704        // Make sure there is a minimum chip width so the user can ALWAYS
705        // tap a chip without difficulty.
706        int width = Math.max(iconWidth * 2, textWidth + startPadding + mChipTextEndPadding
707                + iconWidth + backgroundPadding.left + backgroundPadding.right);
708
709        // Create the background of the chip.
710        result.bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
711        final Canvas canvas = new Canvas(result.bitmap);
712
713        // Check if the background drawable is set via attr
714        if (overrideBackgroundDrawable != null) {
715            overrideBackgroundDrawable.setBounds(0, 0, width, height);
716            overrideBackgroundDrawable.draw(canvas);
717        } else {
718            // Draw the default chip background
719            mWorkPaint.reset();
720            mWorkPaint.setColor(backgroundColor);
721            final float radius = height / 2;
722            canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius,
723                    mWorkPaint);
724        }
725
726        // Draw the text vertically aligned
727        int textX = shouldPositionAvatarOnRight() ?
728                mChipTextEndPadding + backgroundPadding.left :
729                width - backgroundPadding.right - mChipTextEndPadding - textWidth;
730        canvas.drawText(ellipsizedText, 0, ellipsizedText.length(),
731                textX, getTextYOffset(height), paint);
732
733        // Set the variables that are needed to draw the icon bitmap once it's loaded
734        int iconX = shouldPositionAvatarOnRight() ? width - backgroundPadding.right - iconWidth :
735                backgroundPadding.left;
736        result.left = iconX;
737        result.top = backgroundPadding.top;
738        result.right = iconX + iconWidth;
739        result.bottom = height - backgroundPadding.bottom;
740
741        return result;
742    }
743
744    /**
745     * Helper function that draws the loaded icon bitmap into the chips bitmap
746     */
747    private void drawIcon(ChipBitmapContainer bitMapResult, Bitmap icon) {
748        final Canvas canvas = new Canvas(bitMapResult.bitmap);
749        final RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight());
750        final RectF dst = new RectF(bitMapResult.left, bitMapResult.top, bitMapResult.right,
751                bitMapResult.bottom);
752        drawIconOnCanvas(icon, canvas, src, dst);
753    }
754
755    /**
756     * Returns true if the avatar should be positioned at the right edge of the chip.
757     * Takes into account both the set avatar position (start or end) as well as whether
758     * the layout direction is LTR or RTL.
759     */
760    private boolean shouldPositionAvatarOnRight() {
761        final boolean isRtl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ?
762                getLayoutDirection() == LAYOUT_DIRECTION_RTL : false;
763        final boolean assignedPosition = mAvatarPosition == AVATAR_POSITION_END;
764        // If in Rtl mode, the position should be flipped.
765        return isRtl ? !assignedPosition : assignedPosition;
766    }
767
768    /**
769     * Returns the avatar icon to use for this recipient entry. Returns null if we don't want to
770     * draw an icon for this recipient.
771     */
772    private void loadAvatarIcon(final RecipientEntry contact,
773            final ChipBitmapContainer bitmapContainer) {
774        // Don't draw photos for recipients that have been typed in OR generated on the fly.
775        long contactId = contact.getContactId();
776        boolean drawPhotos = isPhoneQuery() ?
777                contactId != RecipientEntry.INVALID_CONTACT
778                : (contactId != RecipientEntry.INVALID_CONTACT
779                        && contactId != RecipientEntry.GENERATED_CONTACT);
780
781        if (drawPhotos) {
782            final byte[] origPhotoBytes = contact.getPhotoBytes();
783            // There may not be a photo yet if anything but the first contact address
784            // was selected.
785            if (origPhotoBytes == null) {
786                // TODO: cache this in the recipient entry?
787                getAdapter().fetchPhoto(contact, new PhotoManager.PhotoManagerCallback() {
788                    @Override
789                    public void onPhotoBytesPopulated() {
790                        // Call through to the async version which will ensure
791                        // proper threading.
792                        onPhotoBytesAsynchronouslyPopulated();
793                    }
794
795                    @Override
796                    public void onPhotoBytesAsynchronouslyPopulated() {
797                        final byte[] loadedPhotoBytes = contact.getPhotoBytes();
798                        final Bitmap icon = BitmapFactory.decodeByteArray(loadedPhotoBytes, 0,
799                                loadedPhotoBytes.length);
800                        tryDrawAndInvalidate(icon);
801                    }
802
803                    @Override
804                    public void onPhotoBytesAsyncLoadFailed() {
805                        // TODO: can the scaled down default photo be cached?
806                        tryDrawAndInvalidate(mDefaultContactPhoto);
807                    }
808
809                    private void tryDrawAndInvalidate(Bitmap icon) {
810                        drawIcon(bitmapContainer, icon);
811                        // The caller might originated from a background task. However, if the
812                        // background task has already completed, the view might be already drawn
813                        // on the UI but the callback would happen on the background thread.
814                        // So if we are on a background thread, post an invalidate call to the UI.
815                        if (Looper.myLooper() == Looper.getMainLooper()) {
816                            // The view might not redraw itself since it's loaded asynchronously
817                            invalidate();
818                        } else {
819                            post(new Runnable() {
820                                @Override
821                                public void run() {
822                                    invalidate();
823                                }
824                            });
825                        }
826                    }
827                });
828            } else {
829                final Bitmap icon = BitmapFactory.decodeByteArray(origPhotoBytes, 0,
830                        origPhotoBytes.length);
831                drawIcon(bitmapContainer, icon);
832            }
833        }
834    }
835
836    /**
837     * Get the background drawable for a RecipientChip.
838     */
839    // Visible for testing.
840    /* package */Drawable getChipBackground(RecipientEntry contact) {
841        return contact.isValid() ? mChipBackground : mInvalidChipBackground;
842    }
843
844    private int getDefaultChipBackgroundColor(RecipientEntry contact) {
845        return getResources().getColor(contact.isValid() ? R.color.chip_background :
846                R.color.chip_background_invalid);
847    }
848
849    /**
850     * Given a height, returns a Y offset that will draw the text in the middle of the height.
851     */
852    protected float getTextYOffset(int height) {
853        return height - ((height - mTextHeight) / 2);
854    }
855
856    /**
857     * Draws the icon onto the canvas given the source rectangle of the bitmap and the destination
858     * rectangle of the canvas.
859     */
860    protected void drawIconOnCanvas(Bitmap icon, Canvas canvas, RectF src, RectF dst) {
861        final Matrix matrix = new Matrix();
862
863        // Draw bitmap through shader first.
864        final BitmapShader shader = new BitmapShader(icon, TileMode.CLAMP, TileMode.CLAMP);
865        matrix.reset();
866
867        // Fit bitmap to bounds.
868        matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);
869
870        shader.setLocalMatrix(matrix);
871        mWorkPaint.reset();
872        mWorkPaint.setShader(shader);
873        mWorkPaint.setAntiAlias(true);
874        mWorkPaint.setFilterBitmap(true);
875        mWorkPaint.setDither(true);
876        canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f, mWorkPaint);
877
878        // Then draw the border.
879        final float borderWidth = 1f;
880        mWorkPaint.reset();
881        mWorkPaint.setColor(Color.TRANSPARENT);
882        mWorkPaint.setStyle(Style.STROKE);
883        mWorkPaint.setStrokeWidth(borderWidth);
884        mWorkPaint.setAntiAlias(true);
885        canvas.drawCircle(dst.centerX(), dst.centerY(), dst.width() / 2f - borderWidth / 2, mWorkPaint);
886
887        mWorkPaint.reset();
888    }
889
890    private DrawableRecipientChip constructChipSpan(RecipientEntry contact, boolean pressed) {
891        TextPaint paint = getPaint();
892        float defaultSize = paint.getTextSize();
893        int defaultColor = paint.getColor();
894
895        Bitmap tmpBitmap;
896        if (pressed) {
897            tmpBitmap = createSelectedChip(contact, paint);
898
899        } else {
900            tmpBitmap = createUnselectedChip(contact, paint);
901        }
902
903        // Pass the full text, un-ellipsized, to the chip.
904        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
905        result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
906        VisibleRecipientChip recipientChip =
907                new VisibleRecipientChip(result, contact);
908        recipientChip.setExtraMargin(mLineSpacingExtra);
909        // Return text to the original size.
910        paint.setTextSize(defaultSize);
911        paint.setColor(defaultColor);
912        return recipientChip;
913    }
914
915    /**
916     * Calculate the bottom of the line the chip will be located on using:
917     * 1) which line the chip appears on
918     * 2) the height of a chip
919     * 3) padding built into the edit text view
920     */
921    private int calculateOffsetFromBottom(int line) {
922        // Line offsets start at zero.
923        int actualLine = getLineCount() - (line + 1);
924        return -((actualLine * ((int) mChipHeight) + getPaddingBottom()) + getPaddingTop())
925                + getDropDownVerticalOffset();
926    }
927
928    /**
929     * Calculate the offset from bottom of the EditText to top of the provided line.
930     */
931    private int calculateOffsetFromBottomToTop(int line) {
932        return -(int) ((mChipHeight + (2 * mLineSpacingExtra)) * (Math
933                .abs(getLineCount() - line)) + getPaddingBottom());
934    }
935
936    /**
937     * Get the max amount of space a chip can take up. The formula takes into
938     * account the width of the EditTextView, any view padding, and padding
939     * that will be added to the chip.
940     */
941    private float calculateAvailableWidth() {
942        return getWidth() - getPaddingLeft() - getPaddingRight() - mChipTextStartPadding
943                - mChipTextEndPadding;
944    }
945
946
947    private void setChipDimensions(Context context, AttributeSet attrs) {
948        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecipientEditTextView, 0,
949                0);
950        Resources r = getContext().getResources();
951
952        mChipBackground = a.getDrawable(R.styleable.RecipientEditTextView_chipBackground);
953        mChipBackgroundPressed = a
954                .getDrawable(R.styleable.RecipientEditTextView_chipBackgroundPressed);
955        mInvalidChipBackground = a
956                .getDrawable(R.styleable.RecipientEditTextView_invalidChipBackground);
957        mChipDelete = a.getDrawable(R.styleable.RecipientEditTextView_chipDelete);
958        if (mChipDelete == null) {
959            mChipDelete = r.getDrawable(R.drawable.ic_cancel_wht_24dp);
960        }
961        mChipTextStartPadding = mChipTextEndPadding
962                = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipPadding, -1);
963        if (mChipTextStartPadding == -1) {
964            mChipTextStartPadding = mChipTextEndPadding =
965                    (int) r.getDimension(R.dimen.chip_padding);
966        }
967        // xml-overrides for each individual padding
968        // TODO: add these to attr?
969        int overridePadding = (int) r.getDimension(R.dimen.chip_padding_start);
970        if (overridePadding >= 0) {
971            mChipTextStartPadding = overridePadding;
972        }
973        overridePadding = (int) r.getDimension(R.dimen.chip_padding_end);
974        if (overridePadding >= 0) {
975            mChipTextEndPadding = overridePadding;
976        }
977
978        mDefaultContactPhoto = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture);
979
980        mMoreItem = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.more_item, null);
981
982        mChipHeight = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipHeight, -1);
983        if (mChipHeight == -1) {
984            mChipHeight = r.getDimension(R.dimen.chip_height);
985        }
986        mChipFontSize = a.getDimensionPixelSize(R.styleable.RecipientEditTextView_chipFontSize, -1);
987        if (mChipFontSize == -1) {
988            mChipFontSize = r.getDimension(R.dimen.chip_text_size);
989        }
990        mAvatarPosition =
991                a.getInt(R.styleable.RecipientEditTextView_avatarPosition, AVATAR_POSITION_START);
992        mDisableDelete = a.getBoolean(R.styleable.RecipientEditTextView_disableDelete, false);
993
994        mMaxLines = r.getInteger(R.integer.chips_max_lines);
995        mLineSpacingExtra = r.getDimensionPixelOffset(R.dimen.line_spacing_extra);
996
997        a.recycle();
998    }
999
1000    // Visible for testing.
1001    /* package */ void setMoreItem(TextView moreItem) {
1002        mMoreItem = moreItem;
1003    }
1004
1005
1006    // Visible for testing.
1007    /* package */ void setChipBackground(Drawable chipBackground) {
1008        mChipBackground = chipBackground;
1009    }
1010
1011    // Visible for testing.
1012    /* package */ void setChipHeight(int height) {
1013        mChipHeight = height;
1014    }
1015
1016    public float getChipHeight() {
1017        return mChipHeight;
1018    }
1019
1020    /**
1021     * Set whether to shrink the recipients field such that at most
1022     * one line of recipients chips are shown when the field loses
1023     * focus. By default, the number of displayed recipients will be
1024     * limited and a "more" chip will be shown when focus is lost.
1025     * @param shrink
1026     */
1027    public void setOnFocusListShrinkRecipients(boolean shrink) {
1028        mShouldShrink = shrink;
1029    }
1030
1031    @Override
1032    public void onSizeChanged(int width, int height, int oldw, int oldh) {
1033        super.onSizeChanged(width, height, oldw, oldh);
1034        if (width != 0 && height != 0) {
1035            if (mPendingChipsCount > 0) {
1036                postHandlePendingChips();
1037            } else {
1038                checkChipWidths();
1039            }
1040        }
1041        // Try to find the scroll view parent, if it exists.
1042        if (mScrollView == null && !mTriedGettingScrollView) {
1043            ViewParent parent = getParent();
1044            while (parent != null && !(parent instanceof ScrollView)) {
1045                parent = parent.getParent();
1046            }
1047            if (parent != null) {
1048                mScrollView = (ScrollView) parent;
1049            }
1050            mTriedGettingScrollView = true;
1051        }
1052    }
1053
1054    private void postHandlePendingChips() {
1055        mHandler.removeCallbacks(mHandlePendingChips);
1056        mHandler.post(mHandlePendingChips);
1057    }
1058
1059    private void checkChipWidths() {
1060        // Check the widths of the associated chips.
1061        DrawableRecipientChip[] chips = getSortedRecipients();
1062        if (chips != null) {
1063            Rect bounds;
1064            for (DrawableRecipientChip chip : chips) {
1065                bounds = chip.getBounds();
1066                if (getWidth() > 0 && bounds.right - bounds.left >
1067                        getWidth() - getPaddingLeft() - getPaddingRight()) {
1068                    // Need to redraw that chip.
1069                    replaceChip(chip, chip.getEntry());
1070                }
1071            }
1072        }
1073    }
1074
1075    // Visible for testing.
1076    /*package*/ void handlePendingChips() {
1077        if (getViewWidth() <= 0) {
1078            // The widget has not been sized yet.
1079            // This will be called as a result of onSizeChanged
1080            // at a later point.
1081            return;
1082        }
1083        if (mPendingChipsCount <= 0) {
1084            return;
1085        }
1086
1087        synchronized (mPendingChips) {
1088            Editable editable = getText();
1089            // Tokenize!
1090            if (mPendingChipsCount <= MAX_CHIPS_PARSED) {
1091                for (int i = 0; i < mPendingChips.size(); i++) {
1092                    String current = mPendingChips.get(i);
1093                    int tokenStart = editable.toString().indexOf(current);
1094                    // Always leave a space at the end between tokens.
1095                    int tokenEnd = tokenStart + current.length() - 1;
1096                    if (tokenStart >= 0) {
1097                        // When we have a valid token, include it with the token
1098                        // to the left.
1099                        if (tokenEnd < editable.length() - 2
1100                                && editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) {
1101                            tokenEnd++;
1102                        }
1103                        createReplacementChip(tokenStart, tokenEnd, editable, i < CHIP_LIMIT
1104                                || !mShouldShrink);
1105                    }
1106                    mPendingChipsCount--;
1107                }
1108                sanitizeEnd();
1109            } else {
1110                mNoChips = true;
1111            }
1112
1113            if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0
1114                    && mTemporaryRecipients.size() <= RecipientAlternatesAdapter.MAX_LOOKUPS) {
1115                if (hasFocus() || mTemporaryRecipients.size() < CHIP_LIMIT) {
1116                    new RecipientReplacementTask().execute();
1117                    mTemporaryRecipients = null;
1118                } else {
1119                    // Create the "more" chip
1120                    mIndividualReplacements = new IndividualReplacementTask();
1121                    mIndividualReplacements.execute(new ArrayList<DrawableRecipientChip>(
1122                            mTemporaryRecipients.subList(0, CHIP_LIMIT)));
1123                    if (mTemporaryRecipients.size() > CHIP_LIMIT) {
1124                        mTemporaryRecipients = new ArrayList<DrawableRecipientChip>(
1125                                mTemporaryRecipients.subList(CHIP_LIMIT,
1126                                        mTemporaryRecipients.size()));
1127                    } else {
1128                        mTemporaryRecipients = null;
1129                    }
1130                    createMoreChip();
1131                }
1132            } else {
1133                // There are too many recipients to look up, so just fall back
1134                // to showing addresses for all of them.
1135                mTemporaryRecipients = null;
1136                createMoreChip();
1137            }
1138            mPendingChipsCount = 0;
1139            mPendingChips.clear();
1140        }
1141    }
1142
1143    // Visible for testing.
1144    /*package*/ int getViewWidth() {
1145        return getWidth();
1146    }
1147
1148    /**
1149     * Remove any characters after the last valid chip.
1150     */
1151    // Visible for testing.
1152    /*package*/ void sanitizeEnd() {
1153        // Don't sanitize while we are waiting for pending chips to complete.
1154        if (mPendingChipsCount > 0) {
1155            return;
1156        }
1157        // Find the last chip; eliminate any commit characters after it.
1158        DrawableRecipientChip[] chips = getSortedRecipients();
1159        Spannable spannable = getSpannable();
1160        if (chips != null && chips.length > 0) {
1161            int end;
1162            mMoreChip = getMoreChip();
1163            if (mMoreChip != null) {
1164                end = spannable.getSpanEnd(mMoreChip);
1165            } else {
1166                end = getSpannable().getSpanEnd(getLastChip());
1167            }
1168            Editable editable = getText();
1169            int length = editable.length();
1170            if (length > end) {
1171                // See what characters occur after that and eliminate them.
1172                if (Log.isLoggable(TAG, Log.DEBUG)) {
1173                    Log.d(TAG, "There were extra characters after the last tokenizable entry."
1174                            + editable);
1175                }
1176                editable.delete(end + 1, length);
1177            }
1178        }
1179    }
1180
1181    /**
1182     * Create a chip that represents just the email address of a recipient. At some later
1183     * point, this chip will be attached to a real contact entry, if one exists.
1184     */
1185    // VisibleForTesting
1186    void createReplacementChip(int tokenStart, int tokenEnd, Editable editable,
1187            boolean visible) {
1188        if (alreadyHasChip(tokenStart, tokenEnd)) {
1189            // There is already a chip present at this location.
1190            // Don't recreate it.
1191            return;
1192        }
1193        String token = editable.toString().substring(tokenStart, tokenEnd);
1194        final String trimmedToken = token.trim();
1195        int commitCharIndex = trimmedToken.lastIndexOf(COMMIT_CHAR_COMMA);
1196        if (commitCharIndex != -1 && commitCharIndex == trimmedToken.length() - 1) {
1197            token = trimmedToken.substring(0, trimmedToken.length() - 1);
1198        }
1199        RecipientEntry entry = createTokenizedEntry(token);
1200        if (entry != null) {
1201            DrawableRecipientChip chip = null;
1202            try {
1203                if (!mNoChips) {
1204                    chip = visible ?
1205                            constructChipSpan(entry, false) : new InvisibleRecipientChip(entry);
1206                }
1207            } catch (NullPointerException e) {
1208                Log.e(TAG, e.getMessage(), e);
1209            }
1210            editable.setSpan(chip, tokenStart, tokenEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1211            // Add this chip to the list of entries "to replace"
1212            if (chip != null) {
1213                if (mTemporaryRecipients == null) {
1214                    mTemporaryRecipients = new ArrayList<DrawableRecipientChip>();
1215                }
1216                chip.setOriginalText(token);
1217                mTemporaryRecipients.add(chip);
1218            }
1219        }
1220    }
1221
1222    private static boolean isPhoneNumber(String number) {
1223        // TODO: replace this function with libphonenumber's isPossibleNumber (see
1224        // PhoneNumberUtil). One complication is that it requires the sender's region which
1225        // comes from the CurrentCountryIso. For now, let's just do this simple match.
1226        if (TextUtils.isEmpty(number)) {
1227            return false;
1228        }
1229
1230        Matcher match = PHONE_PATTERN.matcher(number);
1231        return match.matches();
1232    }
1233
1234    // VisibleForTesting
1235    RecipientEntry createTokenizedEntry(final String token) {
1236        if (TextUtils.isEmpty(token)) {
1237            return null;
1238        }
1239        if (isPhoneQuery() && isPhoneNumber(token)) {
1240            return RecipientEntry.constructFakePhoneEntry(token, true);
1241        }
1242        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token);
1243        String display = null;
1244        boolean isValid = isValid(token);
1245        if (isValid && tokens != null && tokens.length > 0) {
1246            // If we can get a name from tokenizing, then generate an entry from
1247            // this.
1248            display = tokens[0].getName();
1249            if (!TextUtils.isEmpty(display)) {
1250                return RecipientEntry.constructGeneratedEntry(display, tokens[0].getAddress(),
1251                        isValid);
1252            } else {
1253                display = tokens[0].getAddress();
1254                if (!TextUtils.isEmpty(display)) {
1255                    return RecipientEntry.constructFakeEntry(display, isValid);
1256                }
1257            }
1258        }
1259        // Unable to validate the token or to create a valid token from it.
1260        // Just create a chip the user can edit.
1261        String validatedToken = null;
1262        if (mValidator != null && !isValid) {
1263            // Try fixing up the entry using the validator.
1264            validatedToken = mValidator.fixText(token).toString();
1265            if (!TextUtils.isEmpty(validatedToken)) {
1266                if (validatedToken.contains(token)) {
1267                    // protect against the case of a validator with a null
1268                    // domain,
1269                    // which doesn't add a domain to the token
1270                    Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(validatedToken);
1271                    if (tokenized.length > 0) {
1272                        validatedToken = tokenized[0].getAddress();
1273                        isValid = true;
1274                    }
1275                } else {
1276                    // We ran into a case where the token was invalid and
1277                    // removed
1278                    // by the validator. In this case, just use the original
1279                    // token
1280                    // and let the user sort out the error chip.
1281                    validatedToken = null;
1282                    isValid = false;
1283                }
1284            }
1285        }
1286        // Otherwise, fallback to just creating an editable email address chip.
1287        return RecipientEntry.constructFakeEntry(
1288                !TextUtils.isEmpty(validatedToken) ? validatedToken : token, isValid);
1289    }
1290
1291    private boolean isValid(String text) {
1292        return mValidator == null ? true : mValidator.isValid(text);
1293    }
1294
1295    private static String tokenizeAddress(String destination) {
1296        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination);
1297        if (tokens != null && tokens.length > 0) {
1298            return tokens[0].getAddress();
1299        }
1300        return destination;
1301    }
1302
1303    @Override
1304    public void setTokenizer(Tokenizer tokenizer) {
1305        mTokenizer = tokenizer;
1306        super.setTokenizer(mTokenizer);
1307    }
1308
1309    @Override
1310    public void setValidator(Validator validator) {
1311        mValidator = validator;
1312        super.setValidator(validator);
1313    }
1314
1315    /**
1316     * We cannot use the default mechanism for replaceText. Instead,
1317     * we override onItemClickListener so we can get all the associated
1318     * contact information including display text, address, and id.
1319     */
1320    @Override
1321    protected void replaceText(CharSequence text) {
1322        return;
1323    }
1324
1325    /**
1326     * Dismiss any selected chips when the back key is pressed.
1327     */
1328    @Override
1329    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
1330        if (keyCode == KeyEvent.KEYCODE_BACK && mSelectedChip != null) {
1331            clearSelectedChip();
1332            return true;
1333        }
1334        return super.onKeyPreIme(keyCode, event);
1335    }
1336
1337    /**
1338     * Monitor key presses in this view to see if the user types
1339     * any commit keys, which consist of ENTER, TAB, or DPAD_CENTER.
1340     * If the user has entered text that has contact matches and types
1341     * a commit key, create a chip from the topmost matching contact.
1342     * If the user has entered text that has no contact matches and types
1343     * a commit key, then create a chip from the text they have entered.
1344     */
1345    @Override
1346    public boolean onKeyUp(int keyCode, KeyEvent event) {
1347        switch (keyCode) {
1348            case KeyEvent.KEYCODE_TAB:
1349                if (event.hasNoModifiers()) {
1350                    if (mSelectedChip != null) {
1351                        clearSelectedChip();
1352                    } else {
1353                        commitDefault();
1354                    }
1355                }
1356                break;
1357        }
1358        return super.onKeyUp(keyCode, event);
1359    }
1360
1361    private boolean focusNext() {
1362        View next = focusSearch(View.FOCUS_DOWN);
1363        if (next != null) {
1364            next.requestFocus();
1365            return true;
1366        }
1367        return false;
1368    }
1369
1370    /**
1371     * Create a chip from the default selection. If the popup is showing, the
1372     * default is the selected item (if one is selected), or the first item, in the popup
1373     * suggestions list. Otherwise, it is whatever the user had typed in. End represents where the
1374     * tokenizer should search for a token to turn into a chip.
1375     * @return If a chip was created from a real contact.
1376     */
1377    private boolean commitDefault() {
1378        // If there is no tokenizer, don't try to commit.
1379        if (mTokenizer == null) {
1380            return false;
1381        }
1382        Editable editable = getText();
1383        int end = getSelectionEnd();
1384        int start = mTokenizer.findTokenStart(editable, end);
1385
1386        if (shouldCreateChip(start, end)) {
1387            int whatEnd = mTokenizer.findTokenEnd(getText(), start);
1388            // In the middle of chip; treat this as an edit
1389            // and commit the whole token.
1390            whatEnd = movePastTerminators(whatEnd);
1391            if (whatEnd != getSelectionEnd()) {
1392                handleEdit(start, whatEnd);
1393                return true;
1394            }
1395            return commitChip(start, end , editable);
1396        }
1397        return false;
1398    }
1399
1400    private void commitByCharacter() {
1401        // We can't possibly commit by character if we can't tokenize.
1402        if (mTokenizer == null) {
1403            return;
1404        }
1405        Editable editable = getText();
1406        int end = getSelectionEnd();
1407        int start = mTokenizer.findTokenStart(editable, end);
1408        if (shouldCreateChip(start, end)) {
1409            commitChip(start, end, editable);
1410        }
1411        setSelection(getText().length());
1412    }
1413
1414    private boolean commitChip(int start, int end, Editable editable) {
1415        ListAdapter adapter = getAdapter();
1416        if (adapter != null && adapter.getCount() > 0 && enoughToFilter()
1417                && end == getSelectionEnd() && !isPhoneQuery()) {
1418            // let's choose the selected or first entry if only the input text is NOT an email
1419            // address so we won't try to replace the user's potentially correct but
1420            // new/unencountered email input
1421            if (!isValidEmailAddress(editable.toString().substring(start, end).trim())) {
1422                final int selectedPosition = getListSelection();
1423                if (selectedPosition == -1) {
1424                    // Nothing is selected; use the first item
1425                    submitItemAtPosition(0);
1426                } else {
1427                    submitItemAtPosition(selectedPosition);
1428                }
1429            }
1430            dismissDropDown();
1431            return true;
1432        } else {
1433            int tokenEnd = mTokenizer.findTokenEnd(editable, start);
1434            if (editable.length() > tokenEnd + 1) {
1435                char charAt = editable.charAt(tokenEnd + 1);
1436                if (charAt == COMMIT_CHAR_COMMA || charAt == COMMIT_CHAR_SEMICOLON) {
1437                    tokenEnd++;
1438                }
1439            }
1440            String text = editable.toString().substring(start, tokenEnd).trim();
1441            clearComposingText();
1442            if (text != null && text.length() > 0 && !text.equals(" ")) {
1443                RecipientEntry entry = createTokenizedEntry(text);
1444                if (entry != null) {
1445                    QwertyKeyListener.markAsReplaced(editable, start, end, "");
1446                    CharSequence chipText = createChip(entry, false);
1447                    if (chipText != null && start > -1 && end > -1) {
1448                        editable.replace(start, end, chipText);
1449                    }
1450                }
1451                // Only dismiss the dropdown if it is related to the text we
1452                // just committed.
1453                // For paste, it may not be as there are possibly multiple
1454                // tokens being added.
1455                if (end == getSelectionEnd()) {
1456                    dismissDropDown();
1457                }
1458                sanitizeBetween();
1459                return true;
1460            }
1461        }
1462        return false;
1463    }
1464
1465    // Visible for testing.
1466    /* package */ void sanitizeBetween() {
1467        // Don't sanitize while we are waiting for content to chipify.
1468        if (mPendingChipsCount > 0) {
1469            return;
1470        }
1471        // Find the last chip.
1472        DrawableRecipientChip[] recips = getSortedRecipients();
1473        if (recips != null && recips.length > 0) {
1474            DrawableRecipientChip last = recips[recips.length - 1];
1475            DrawableRecipientChip beforeLast = null;
1476            if (recips.length > 1) {
1477                beforeLast = recips[recips.length - 2];
1478            }
1479            int startLooking = 0;
1480            int end = getSpannable().getSpanStart(last);
1481            if (beforeLast != null) {
1482                startLooking = getSpannable().getSpanEnd(beforeLast);
1483                Editable text = getText();
1484                if (startLooking == -1 || startLooking > text.length() - 1) {
1485                    // There is nothing after this chip.
1486                    return;
1487                }
1488                if (text.charAt(startLooking) == ' ') {
1489                    startLooking++;
1490                }
1491            }
1492            if (startLooking >= 0 && end >= 0 && startLooking < end) {
1493                getText().delete(startLooking, end);
1494            }
1495        }
1496    }
1497
1498    private boolean shouldCreateChip(int start, int end) {
1499        return !mNoChips && hasFocus() && enoughToFilter() && !alreadyHasChip(start, end);
1500    }
1501
1502    private boolean alreadyHasChip(int start, int end) {
1503        if (mNoChips) {
1504            return true;
1505        }
1506        DrawableRecipientChip[] chips =
1507                getSpannable().getSpans(start, end, DrawableRecipientChip.class);
1508        if ((chips == null || chips.length == 0)) {
1509            return false;
1510        }
1511        return true;
1512    }
1513
1514    private void handleEdit(int start, int end) {
1515        if (start == -1 || end == -1) {
1516            // This chip no longer exists in the field.
1517            dismissDropDown();
1518            return;
1519        }
1520        // This is in the middle of a chip, so select out the whole chip
1521        // and commit it.
1522        Editable editable = getText();
1523        setSelection(end);
1524        String text = getText().toString().substring(start, end);
1525        if (!TextUtils.isEmpty(text)) {
1526            RecipientEntry entry = RecipientEntry.constructFakeEntry(text, isValid(text));
1527            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1528            CharSequence chipText = createChip(entry, false);
1529            int selEnd = getSelectionEnd();
1530            if (chipText != null && start > -1 && selEnd > -1) {
1531                editable.replace(start, selEnd, chipText);
1532            }
1533        }
1534        dismissDropDown();
1535    }
1536
1537    /**
1538     * If there is a selected chip, delegate the key events
1539     * to the selected chip.
1540     */
1541    @Override
1542    public boolean onKeyDown(int keyCode, KeyEvent event) {
1543        if (mSelectedChip != null && keyCode == KeyEvent.KEYCODE_DEL) {
1544            if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
1545                mAlternatesPopup.dismiss();
1546            }
1547            removeChip(mSelectedChip);
1548        }
1549
1550        switch (keyCode) {
1551            case KeyEvent.KEYCODE_ENTER:
1552            case KeyEvent.KEYCODE_DPAD_CENTER:
1553                if (event.hasNoModifiers()) {
1554                    if (commitDefault()) {
1555                        return true;
1556                    }
1557                    if (mSelectedChip != null) {
1558                        clearSelectedChip();
1559                        return true;
1560                    } else if (focusNext()) {
1561                        return true;
1562                    }
1563                }
1564                break;
1565        }
1566
1567        return super.onKeyDown(keyCode, event);
1568    }
1569
1570    // Visible for testing.
1571    /* package */ Spannable getSpannable() {
1572        return getText();
1573    }
1574
1575    private int getChipStart(DrawableRecipientChip chip) {
1576        return getSpannable().getSpanStart(chip);
1577    }
1578
1579    private int getChipEnd(DrawableRecipientChip chip) {
1580        return getSpannable().getSpanEnd(chip);
1581    }
1582
1583    /**
1584     * Instead of filtering on the entire contents of the edit box,
1585     * this subclass method filters on the range from
1586     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
1587     * if the length of that range meets or exceeds {@link #getThreshold}
1588     * and makes sure that the range is not already a Chip.
1589     */
1590    @Override
1591    protected void performFiltering(CharSequence text, int keyCode) {
1592        boolean isCompletedToken = isCompletedToken(text);
1593        if (enoughToFilter() && !isCompletedToken) {
1594            int end = getSelectionEnd();
1595            int start = mTokenizer.findTokenStart(text, end);
1596            // If this is a RecipientChip, don't filter
1597            // on its contents.
1598            Spannable span = getSpannable();
1599            DrawableRecipientChip[] chips = span.getSpans(start, end, DrawableRecipientChip.class);
1600            if (chips != null && chips.length > 0) {
1601                dismissDropDown();
1602                return;
1603            }
1604        } else if (isCompletedToken) {
1605            dismissDropDown();
1606            return;
1607        }
1608        super.performFiltering(text, keyCode);
1609    }
1610
1611    // Visible for testing.
1612    /*package*/ boolean isCompletedToken(CharSequence text) {
1613        if (TextUtils.isEmpty(text)) {
1614            return false;
1615        }
1616        // Check to see if this is a completed token before filtering.
1617        int end = text.length();
1618        int start = mTokenizer.findTokenStart(text, end);
1619        String token = text.toString().substring(start, end).trim();
1620        if (!TextUtils.isEmpty(token)) {
1621            char atEnd = token.charAt(token.length() - 1);
1622            return atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON;
1623        }
1624        return false;
1625    }
1626
1627    /**
1628     * Clears the selected chip if there is one (and dismissing any popups related to the selected
1629     * chip in the process).
1630     */
1631    public void clearSelectedChip() {
1632        if (mSelectedChip != null) {
1633            unselectChip(mSelectedChip);
1634            mSelectedChip = null;
1635        }
1636        setCursorVisible(true);
1637    }
1638
1639    /**
1640     * Monitor touch events in the RecipientEditTextView.
1641     * If the view does not have focus, any tap on the view
1642     * will just focus the view. If the view has focus, determine
1643     * if the touch target is a recipient chip. If it is and the chip
1644     * is not selected, select it and clear any other selected chips.
1645     * If it isn't, then select that chip.
1646     */
1647    @Override
1648    public boolean onTouchEvent(MotionEvent event) {
1649        if (!isFocused()) {
1650            // Ignore any chip taps until this view is focused.
1651            return super.onTouchEvent(event);
1652        }
1653        boolean handled = super.onTouchEvent(event);
1654        int action = event.getAction();
1655        boolean chipWasSelected = false;
1656        if (mSelectedChip == null) {
1657            mGestureDetector.onTouchEvent(event);
1658        }
1659        if (mCopyAddress == null && action == MotionEvent.ACTION_UP) {
1660            float x = event.getX();
1661            float y = event.getY();
1662            int offset = putOffsetInRange(x, y);
1663            DrawableRecipientChip currentChip = findChip(offset);
1664            if (currentChip != null) {
1665                if (action == MotionEvent.ACTION_UP) {
1666                    if (mSelectedChip != null && mSelectedChip != currentChip) {
1667                        clearSelectedChip();
1668                        mSelectedChip = selectChip(currentChip);
1669                    } else if (mSelectedChip == null) {
1670                        setSelection(getText().length());
1671                        commitDefault();
1672                        mSelectedChip = selectChip(currentChip);
1673                    } else {
1674                        onClick(mSelectedChip);
1675                    }
1676                }
1677                chipWasSelected = true;
1678                handled = true;
1679            } else if (mSelectedChip != null && shouldShowEditableText(mSelectedChip)) {
1680                chipWasSelected = true;
1681            }
1682        }
1683        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
1684            clearSelectedChip();
1685        }
1686        return handled;
1687    }
1688
1689    private void scrollLineIntoView(int line) {
1690        if (mScrollView != null) {
1691            mScrollView.smoothScrollBy(0, calculateOffsetFromBottom(line));
1692        }
1693    }
1694
1695    private void showAlternates(final DrawableRecipientChip currentChip,
1696            final ListPopupWindow alternatesPopup) {
1697        new AsyncTask<Void, Void, ListAdapter>() {
1698            @Override
1699            protected ListAdapter doInBackground(final Void... params) {
1700                return createAlternatesAdapter(currentChip);
1701            }
1702
1703            @Override
1704            protected void onPostExecute(final ListAdapter result) {
1705                if (!mAttachedToWindow) {
1706                    return;
1707                }
1708                int line = getLayout().getLineForOffset(getChipStart(currentChip));
1709                int bottomOffset = calculateOffsetFromBottomToTop(line);
1710
1711                // Align the alternates popup with the left side of the View,
1712                // regardless of the position of the chip tapped.
1713                alternatesPopup.setAnchorView((mAlternatePopupAnchor != null) ?
1714                        mAlternatePopupAnchor : RecipientEditTextView.this);
1715                alternatesPopup.setVerticalOffset(bottomOffset);
1716                alternatesPopup.setAdapter(result);
1717                alternatesPopup.setOnItemClickListener(mAlternatesListener);
1718                // Clear the checked item.
1719                mCheckedItem = -1;
1720                alternatesPopup.show();
1721                ListView listView = alternatesPopup.getListView();
1722                listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
1723                // Checked item would be -1 if the adapter has not
1724                // loaded the view that should be checked yet. The
1725                // variable will be set correctly when onCheckedItemChanged
1726                // is called in a separate thread.
1727                if (mCheckedItem != -1) {
1728                    listView.setItemChecked(mCheckedItem, true);
1729                    mCheckedItem = -1;
1730                }
1731            }
1732        }.execute((Void[]) null);
1733    }
1734
1735    private ListAdapter createAlternatesAdapter(DrawableRecipientChip chip) {
1736        return new RecipientAlternatesAdapter(getContext(), chip.getContactId(),
1737                chip.getDirectoryId(), chip.getLookupKey(), chip.getDataId(),
1738                getAdapter().getQueryType(), this, mDropdownChipLayouter,
1739                constructStateListDeleteDrawable());
1740    }
1741
1742    private ListAdapter createSingleAddressAdapter(DrawableRecipientChip currentChip) {
1743        return new SingleRecipientArrayAdapter(getContext(), currentChip.getEntry(),
1744                mDropdownChipLayouter, constructStateListDeleteDrawable());
1745    }
1746
1747    private StateListDrawable constructStateListDeleteDrawable() {
1748        // Construct the StateListDrawable from deleteDrawable
1749        StateListDrawable deleteDrawable = new StateListDrawable();
1750        if (!mDisableDelete) {
1751            deleteDrawable.addState(new int[]{android.R.attr.state_activated}, mChipDelete);
1752        }
1753        deleteDrawable.addState(new int[0], null);
1754        return deleteDrawable;
1755    }
1756
1757    @Override
1758    public void onCheckedItemChanged(int position) {
1759        ListView listView = mAlternatesPopup.getListView();
1760        if (listView != null && listView.getCheckedItemCount() == 0) {
1761            listView.setItemChecked(position, true);
1762        }
1763        mCheckedItem = position;
1764    }
1765
1766    private int putOffsetInRange(final float x, final float y) {
1767        final int offset;
1768
1769        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
1770            offset = getOffsetForPosition(x, y);
1771        } else {
1772            offset = supportGetOffsetForPosition(x, y);
1773        }
1774
1775        return putOffsetInRange(offset);
1776    }
1777
1778    // TODO: This algorithm will need a lot of tweaking after more people have used
1779    // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
1780    // what comes before the finger.
1781    private int putOffsetInRange(int o) {
1782        int offset = o;
1783        Editable text = getText();
1784        int length = text.length();
1785        // Remove whitespace from end to find "real end"
1786        int realLength = length;
1787        for (int i = length - 1; i >= 0; i--) {
1788            if (text.charAt(i) == ' ') {
1789                realLength--;
1790            } else {
1791                break;
1792            }
1793        }
1794
1795        // If the offset is beyond or at the end of the text,
1796        // leave it alone.
1797        if (offset >= realLength) {
1798            return offset;
1799        }
1800        Editable editable = getText();
1801        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
1802            // Keep walking backward!
1803            offset--;
1804        }
1805        return offset;
1806    }
1807
1808    private static int findText(Editable text, int offset) {
1809        if (text.charAt(offset) != ' ') {
1810            return offset;
1811        }
1812        return -1;
1813    }
1814
1815    private DrawableRecipientChip findChip(int offset) {
1816        DrawableRecipientChip[] chips =
1817                getSpannable().getSpans(0, getText().length(), DrawableRecipientChip.class);
1818        // Find the chip that contains this offset.
1819        for (int i = 0; i < chips.length; i++) {
1820            DrawableRecipientChip chip = chips[i];
1821            int start = getChipStart(chip);
1822            int end = getChipEnd(chip);
1823            if (offset >= start && offset <= end) {
1824                return chip;
1825            }
1826        }
1827        return null;
1828    }
1829
1830    // Visible for testing.
1831    // Use this method to generate text to add to the list of addresses.
1832    /* package */String createAddressText(RecipientEntry entry) {
1833        String display = entry.getDisplayName();
1834        String address = entry.getDestination();
1835        if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
1836            display = null;
1837        }
1838        String trimmedDisplayText;
1839        if (isPhoneQuery() && isPhoneNumber(address)) {
1840            trimmedDisplayText = address.trim();
1841        } else {
1842            if (address != null) {
1843                // Tokenize out the address in case the address already
1844                // contained the username as well.
1845                Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(address);
1846                if (tokenized != null && tokenized.length > 0) {
1847                    address = tokenized[0].getAddress();
1848                }
1849            }
1850            Rfc822Token token = new Rfc822Token(display, address, null);
1851            trimmedDisplayText = token.toString().trim();
1852        }
1853        int index = trimmedDisplayText.indexOf(",");
1854        return mTokenizer != null && !TextUtils.isEmpty(trimmedDisplayText)
1855                && index < trimmedDisplayText.length() - 1 ? (String) mTokenizer
1856                .terminateToken(trimmedDisplayText) : trimmedDisplayText;
1857    }
1858
1859    // Visible for testing.
1860    // Use this method to generate text to display in a chip.
1861    /*package*/ String createChipDisplayText(RecipientEntry entry) {
1862        String display = entry.getDisplayName();
1863        String address = entry.getDestination();
1864        if (TextUtils.isEmpty(display) || TextUtils.equals(display, address)) {
1865            display = null;
1866        }
1867        if (!TextUtils.isEmpty(display)) {
1868            return display;
1869        } else if (!TextUtils.isEmpty(address)){
1870            return address;
1871        } else {
1872            return new Rfc822Token(display, address, null).toString();
1873        }
1874    }
1875
1876    private CharSequence createChip(RecipientEntry entry, boolean pressed) {
1877        final String displayText = createAddressText(entry);
1878        if (TextUtils.isEmpty(displayText)) {
1879            return null;
1880        }
1881        // Always leave a blank space at the end of a chip.
1882        final int textLength = displayText.length() - 1;
1883        final SpannableString  chipText = new SpannableString(displayText);
1884        if (!mNoChips) {
1885            try {
1886                DrawableRecipientChip chip = constructChipSpan(entry, pressed);
1887                chipText.setSpan(chip, 0, textLength,
1888                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1889                chip.setOriginalText(chipText.toString());
1890            } catch (NullPointerException e) {
1891                Log.e(TAG, e.getMessage(), e);
1892                return null;
1893            }
1894        }
1895        onChipCreated(entry);
1896        return chipText;
1897    }
1898
1899    /**
1900     * A callback for subclasses to use to know when a chip was created with the
1901     * given RecipientEntry.
1902     */
1903    protected void onChipCreated(RecipientEntry entry) {}
1904
1905    /**
1906     * When an item in the suggestions list has been clicked, create a chip from the
1907     * contact information of the selected item.
1908     */
1909    @Override
1910    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1911        if (position < 0) {
1912            return;
1913        }
1914
1915        final int charactersTyped = submitItemAtPosition(position);
1916        if (charactersTyped > -1 && mRecipientEntryItemClickedListener != null) {
1917            mRecipientEntryItemClickedListener
1918                    .onRecipientEntryItemClicked(charactersTyped, position);
1919        }
1920    }
1921
1922    private int submitItemAtPosition(int position) {
1923        RecipientEntry entry = createValidatedEntry(getAdapter().getItem(position));
1924        if (entry == null) {
1925            return -1;
1926        }
1927        clearComposingText();
1928
1929        int end = getSelectionEnd();
1930        int start = mTokenizer.findTokenStart(getText(), end);
1931
1932        Editable editable = getText();
1933        QwertyKeyListener.markAsReplaced(editable, start, end, "");
1934        CharSequence chip = createChip(entry, false);
1935        if (chip != null && start >= 0 && end >= 0) {
1936            editable.replace(start, end, chip);
1937        }
1938        sanitizeBetween();
1939
1940        return end - start;
1941    }
1942
1943    private RecipientEntry createValidatedEntry(RecipientEntry item) {
1944        if (item == null) {
1945            return null;
1946        }
1947        final RecipientEntry entry;
1948        // If the display name and the address are the same, or if this is a
1949        // valid contact, but the destination is invalid, then make this a fake
1950        // recipient that is editable.
1951        String destination = item.getDestination();
1952        if (!isPhoneQuery() && item.getContactId() == RecipientEntry.GENERATED_CONTACT) {
1953            entry = RecipientEntry.constructGeneratedEntry(item.getDisplayName(),
1954                    destination, item.isValid());
1955        } else if (RecipientEntry.isCreatedRecipient(item.getContactId())
1956                && (TextUtils.isEmpty(item.getDisplayName())
1957                        || TextUtils.equals(item.getDisplayName(), destination)
1958                        || (mValidator != null && !mValidator.isValid(destination)))) {
1959            entry = RecipientEntry.constructFakeEntry(destination, item.isValid());
1960        } else {
1961            entry = item;
1962        }
1963        return entry;
1964    }
1965
1966    // Visible for testing.
1967    /* package */DrawableRecipientChip[] getSortedRecipients() {
1968        DrawableRecipientChip[] recips = getSpannable()
1969                .getSpans(0, getText().length(), DrawableRecipientChip.class);
1970        ArrayList<DrawableRecipientChip> recipientsList = new ArrayList<DrawableRecipientChip>(
1971                Arrays.asList(recips));
1972        final Spannable spannable = getSpannable();
1973        Collections.sort(recipientsList, new Comparator<DrawableRecipientChip>() {
1974
1975            @Override
1976            public int compare(DrawableRecipientChip first, DrawableRecipientChip second) {
1977                int firstStart = spannable.getSpanStart(first);
1978                int secondStart = spannable.getSpanStart(second);
1979                if (firstStart < secondStart) {
1980                    return -1;
1981                } else if (firstStart > secondStart) {
1982                    return 1;
1983                } else {
1984                    return 0;
1985                }
1986            }
1987        });
1988        return recipientsList.toArray(new DrawableRecipientChip[recipientsList.size()]);
1989    }
1990
1991    @Override
1992    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1993        return false;
1994    }
1995
1996    @Override
1997    public void onDestroyActionMode(ActionMode mode) {
1998    }
1999
2000    @Override
2001    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
2002        return false;
2003    }
2004
2005    /**
2006     * No chips are selectable.
2007     */
2008    @Override
2009    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
2010        return false;
2011    }
2012
2013    // Visible for testing.
2014    /* package */ReplacementDrawableSpan getMoreChip() {
2015        MoreImageSpan[] moreSpans = getSpannable().getSpans(0, getText().length(),
2016                MoreImageSpan.class);
2017        return moreSpans != null && moreSpans.length > 0 ? moreSpans[0] : null;
2018    }
2019
2020    private MoreImageSpan createMoreSpan(int count) {
2021        String moreText = String.format(mMoreItem.getText().toString(), count);
2022        mWorkPaint.set(getPaint());
2023        mWorkPaint.setTextSize(mMoreItem.getTextSize());
2024        mWorkPaint.setColor(mMoreItem.getCurrentTextColor());
2025        final int width = (int) mWorkPaint.measureText(moreText) + mMoreItem.getPaddingLeft()
2026                + mMoreItem.getPaddingRight();
2027        final int height = (int) mChipHeight;
2028        Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
2029        Canvas canvas = new Canvas(drawable);
2030        int adjustedHeight = height;
2031        Layout layout = getLayout();
2032        if (layout != null) {
2033            adjustedHeight -= layout.getLineDescent(0);
2034        }
2035        canvas.drawText(moreText, 0, moreText.length(), 0, adjustedHeight, mWorkPaint);
2036
2037        Drawable result = new BitmapDrawable(getResources(), drawable);
2038        result.setBounds(0, 0, width, height);
2039        return new MoreImageSpan(result);
2040    }
2041
2042    // Visible for testing.
2043    /*package*/ void createMoreChipPlainText() {
2044        // Take the first <= CHIP_LIMIT addresses and get to the end of the second one.
2045        Editable text = getText();
2046        int start = 0;
2047        int end = start;
2048        for (int i = 0; i < CHIP_LIMIT; i++) {
2049            end = movePastTerminators(mTokenizer.findTokenEnd(text, start));
2050            start = end; // move to the next token and get its end.
2051        }
2052        // Now, count total addresses.
2053        start = 0;
2054        int tokenCount = countTokens(text);
2055        MoreImageSpan moreSpan = createMoreSpan(tokenCount - CHIP_LIMIT);
2056        SpannableString chipText = new SpannableString(text.subSequence(end, text.length()));
2057        chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2058        text.replace(end, text.length(), chipText);
2059        mMoreChip = moreSpan;
2060    }
2061
2062    // Visible for testing.
2063    /* package */int countTokens(Editable text) {
2064        int tokenCount = 0;
2065        int start = 0;
2066        while (start < text.length()) {
2067            start = movePastTerminators(mTokenizer.findTokenEnd(text, start));
2068            tokenCount++;
2069            if (start >= text.length()) {
2070                break;
2071            }
2072        }
2073        return tokenCount;
2074    }
2075
2076    /**
2077     * Create the more chip. The more chip is text that replaces any chips that
2078     * do not fit in the pre-defined available space when the
2079     * RecipientEditTextView loses focus.
2080     */
2081    // Visible for testing.
2082    /* package */ void createMoreChip() {
2083        if (mNoChips) {
2084            createMoreChipPlainText();
2085            return;
2086        }
2087
2088        if (!mShouldShrink) {
2089            return;
2090        }
2091        ReplacementDrawableSpan[] tempMore = getSpannable().getSpans(0, getText().length(),
2092                MoreImageSpan.class);
2093        if (tempMore.length > 0) {
2094            getSpannable().removeSpan(tempMore[0]);
2095        }
2096        DrawableRecipientChip[] recipients = getSortedRecipients();
2097
2098        if (recipients == null || recipients.length <= CHIP_LIMIT) {
2099            mMoreChip = null;
2100            return;
2101        }
2102        Spannable spannable = getSpannable();
2103        int numRecipients = recipients.length;
2104        int overage = numRecipients - CHIP_LIMIT;
2105        MoreImageSpan moreSpan = createMoreSpan(overage);
2106        mRemovedSpans = new ArrayList<DrawableRecipientChip>();
2107        int totalReplaceStart = 0;
2108        int totalReplaceEnd = 0;
2109        Editable text = getText();
2110        for (int i = numRecipients - overage; i < recipients.length; i++) {
2111            mRemovedSpans.add(recipients[i]);
2112            if (i == numRecipients - overage) {
2113                totalReplaceStart = spannable.getSpanStart(recipients[i]);
2114            }
2115            if (i == recipients.length - 1) {
2116                totalReplaceEnd = spannable.getSpanEnd(recipients[i]);
2117            }
2118            if (mTemporaryRecipients == null || !mTemporaryRecipients.contains(recipients[i])) {
2119                int spanStart = spannable.getSpanStart(recipients[i]);
2120                int spanEnd = spannable.getSpanEnd(recipients[i]);
2121                recipients[i].setOriginalText(text.toString().substring(spanStart, spanEnd));
2122            }
2123            spannable.removeSpan(recipients[i]);
2124        }
2125        if (totalReplaceEnd < text.length()) {
2126            totalReplaceEnd = text.length();
2127        }
2128        int end = Math.max(totalReplaceStart, totalReplaceEnd);
2129        int start = Math.min(totalReplaceStart, totalReplaceEnd);
2130        SpannableString chipText = new SpannableString(text.subSequence(start, end));
2131        chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2132        text.replace(start, end, chipText);
2133        mMoreChip = moreSpan;
2134        // If adding the +more chip goes over the limit, resize accordingly.
2135        if (!isPhoneQuery() && getLineCount() > mMaxLines) {
2136            setMaxLines(getLineCount());
2137        }
2138    }
2139
2140    /**
2141     * Replace the more chip, if it exists, with all of the recipient chips it had
2142     * replaced when the RecipientEditTextView gains focus.
2143     */
2144    // Visible for testing.
2145    /*package*/ void removeMoreChip() {
2146        if (mMoreChip != null) {
2147            Spannable span = getSpannable();
2148            span.removeSpan(mMoreChip);
2149            mMoreChip = null;
2150            // Re-add the spans that were removed.
2151            if (mRemovedSpans != null && mRemovedSpans.size() > 0) {
2152                // Recreate each removed span.
2153                DrawableRecipientChip[] recipients = getSortedRecipients();
2154                // Start the search for tokens after the last currently visible
2155                // chip.
2156                if (recipients == null || recipients.length == 0) {
2157                    return;
2158                }
2159                int end = span.getSpanEnd(recipients[recipients.length - 1]);
2160                Editable editable = getText();
2161                for (DrawableRecipientChip chip : mRemovedSpans) {
2162                    int chipStart;
2163                    int chipEnd;
2164                    String token;
2165                    // Need to find the location of the chip, again.
2166                    token = (String) chip.getOriginalText();
2167                    // As we find the matching recipient for the remove spans,
2168                    // reduce the size of the string we need to search.
2169                    // That way, if there are duplicates, we always find the correct
2170                    // recipient.
2171                    chipStart = editable.toString().indexOf(token, end);
2172                    end = chipEnd = Math.min(editable.length(), chipStart + token.length());
2173                    // Only set the span if we found a matching token.
2174                    if (chipStart != -1) {
2175                        editable.setSpan(chip, chipStart, chipEnd,
2176                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
2177                    }
2178                }
2179                mRemovedSpans.clear();
2180            }
2181        }
2182    }
2183
2184    /**
2185     * Show specified chip as selected. If the RecipientChip is just an email address,
2186     * selecting the chip will take the contents of the chip and place it at
2187     * the end of the RecipientEditTextView for inline editing. If the
2188     * RecipientChip is a complete contact, then selecting the chip
2189     * will change the background color of the chip, show the delete icon,
2190     * and a popup window with the address in use highlighted and any other
2191     * alternate addresses for the contact.
2192     * @param currentChip Chip to select.
2193     * @return A RecipientChip in the selected state or null if the chip
2194     * just contained an email address.
2195     */
2196    private DrawableRecipientChip selectChip(DrawableRecipientChip currentChip) {
2197        if (shouldShowEditableText(currentChip)) {
2198            CharSequence text = currentChip.getValue();
2199            Editable editable = getText();
2200            Spannable spannable = getSpannable();
2201            int spanStart = spannable.getSpanStart(currentChip);
2202            int spanEnd = spannable.getSpanEnd(currentChip);
2203            spannable.removeSpan(currentChip);
2204            // Don't need leading space if it's the only chip
2205            if (spanEnd - spanStart == editable.length() - 1) {
2206                spanEnd++;
2207            }
2208            editable.delete(spanStart, spanEnd);
2209            setCursorVisible(true);
2210            setSelection(editable.length());
2211            editable.append(text);
2212            return constructChipSpan(
2213                    RecipientEntry.constructFakeEntry((String) text, isValid(text.toString())),
2214                    true);
2215        } else {
2216            int start = getChipStart(currentChip);
2217            int end = getChipEnd(currentChip);
2218            getSpannable().removeSpan(currentChip);
2219            DrawableRecipientChip newChip;
2220            final boolean showAddress =
2221                    currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT ||
2222                    getAdapter().forceShowAddress();
2223            try {
2224                if (showAddress && mNoChips) {
2225                    return null;
2226                }
2227                newChip = constructChipSpan(currentChip.getEntry(), true);
2228            } catch (NullPointerException e) {
2229                Log.e(TAG, e.getMessage(), e);
2230                return null;
2231            }
2232            Editable editable = getText();
2233            QwertyKeyListener.markAsReplaced(editable, start, end, "");
2234            if (start == -1 || end == -1) {
2235                Log.d(TAG, "The chip being selected no longer exists but should.");
2236            } else {
2237                editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2238            }
2239            newChip.setSelected(true);
2240            if (shouldShowEditableText(newChip)) {
2241                scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
2242            }
2243            if (showAddress) {
2244                showAddress(newChip, mAddressPopup);
2245            } else {
2246                showAlternates(newChip, mAlternatesPopup);
2247            }
2248            setCursorVisible(false);
2249            return newChip;
2250        }
2251    }
2252
2253    private boolean shouldShowEditableText(DrawableRecipientChip currentChip) {
2254        long contactId = currentChip.getContactId();
2255        return contactId == RecipientEntry.INVALID_CONTACT
2256                || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
2257    }
2258
2259    private void showAddress(final DrawableRecipientChip currentChip, final ListPopupWindow popup) {
2260        if (!mAttachedToWindow) {
2261            return;
2262        }
2263        int line = getLayout().getLineForOffset(getChipStart(currentChip));
2264        int bottomOffset = calculateOffsetFromBottomToTop(line);
2265        // Align the alternates popup with the left side of the View,
2266        // regardless of the position of the chip tapped.
2267        popup.setAnchorView((mAlternatePopupAnchor != null) ? mAlternatePopupAnchor : this);
2268        popup.setVerticalOffset(bottomOffset);
2269        popup.setAdapter(createSingleAddressAdapter(currentChip));
2270        popup.setOnItemClickListener(new OnItemClickListener() {
2271            @Override
2272            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
2273                unselectChip(currentChip);
2274                popup.dismiss();
2275            }
2276        });
2277        popup.show();
2278        ListView listView = popup.getListView();
2279        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
2280        listView.setItemChecked(0, true);
2281    }
2282
2283    /**
2284     * Remove selection from this chip. Unselecting a RecipientChip will render
2285     * the chip without a delete icon and with an unfocused background. This is
2286     * called when the RecipientChip no longer has focus.
2287     */
2288    private void unselectChip(DrawableRecipientChip chip) {
2289        int start = getChipStart(chip);
2290        int end = getChipEnd(chip);
2291        Editable editable = getText();
2292        mSelectedChip = null;
2293        if (start == -1 || end == -1) {
2294            Log.w(TAG, "The chip doesn't exist or may be a chip a user was editing");
2295            setSelection(editable.length());
2296            commitDefault();
2297        } else {
2298            getSpannable().removeSpan(chip);
2299            QwertyKeyListener.markAsReplaced(editable, start, end, "");
2300            editable.removeSpan(chip);
2301            try {
2302                if (!mNoChips) {
2303                    editable.setSpan(constructChipSpan(chip.getEntry(), false),
2304                            start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2305                }
2306            } catch (NullPointerException e) {
2307                Log.e(TAG, e.getMessage(), e);
2308            }
2309        }
2310        setCursorVisible(true);
2311        setSelection(editable.length());
2312        if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
2313            mAlternatesPopup.dismiss();
2314        }
2315    }
2316
2317    @Override
2318    public void onChipDelete() {
2319        if (mSelectedChip != null) {
2320            removeChip(mSelectedChip);
2321        }
2322        mAddressPopup.dismiss();
2323        mAlternatesPopup.dismiss();
2324    }
2325
2326    /**
2327     * Remove the chip and any text associated with it from the RecipientEditTextView.
2328     */
2329    // Visible for testing.
2330    /* package */void removeChip(DrawableRecipientChip chip) {
2331        Spannable spannable = getSpannable();
2332        int spanStart = spannable.getSpanStart(chip);
2333        int spanEnd = spannable.getSpanEnd(chip);
2334        Editable text = getText();
2335        int toDelete = spanEnd;
2336        boolean wasSelected = chip == mSelectedChip;
2337        // Clear that there is a selected chip before updating any text.
2338        if (wasSelected) {
2339            mSelectedChip = null;
2340        }
2341        // Always remove trailing spaces when removing a chip.
2342        while (toDelete >= 0 && toDelete < text.length() && text.charAt(toDelete) == ' ') {
2343            toDelete++;
2344        }
2345        spannable.removeSpan(chip);
2346        if (spanStart >= 0 && toDelete > 0) {
2347            text.delete(spanStart, toDelete);
2348        }
2349        if (wasSelected) {
2350            clearSelectedChip();
2351        }
2352    }
2353
2354    /**
2355     * Replace this currently selected chip with a new chip
2356     * that uses the contact data provided.
2357     */
2358    // Visible for testing.
2359    /*package*/ void replaceChip(DrawableRecipientChip chip, RecipientEntry entry) {
2360        boolean wasSelected = chip == mSelectedChip;
2361        if (wasSelected) {
2362            mSelectedChip = null;
2363        }
2364        int start = getChipStart(chip);
2365        int end = getChipEnd(chip);
2366        getSpannable().removeSpan(chip);
2367        Editable editable = getText();
2368        CharSequence chipText = createChip(entry, false);
2369        if (chipText != null) {
2370            if (start == -1 || end == -1) {
2371                Log.e(TAG, "The chip to replace does not exist but should.");
2372                editable.insert(0, chipText);
2373            } else {
2374                if (!TextUtils.isEmpty(chipText)) {
2375                    // There may be a space to replace with this chip's new
2376                    // associated space. Check for it
2377                    int toReplace = end;
2378                    while (toReplace >= 0 && toReplace < editable.length()
2379                            && editable.charAt(toReplace) == ' ') {
2380                        toReplace++;
2381                    }
2382                    editable.replace(start, toReplace, chipText);
2383                }
2384            }
2385        }
2386        setCursorVisible(true);
2387        if (wasSelected) {
2388            clearSelectedChip();
2389        }
2390    }
2391
2392    /**
2393     * Handle click events for a chip. When a selected chip receives a click
2394     * event, see if that event was in the delete icon. If so, delete it.
2395     * Otherwise, unselect the chip.
2396     */
2397    public void onClick(DrawableRecipientChip chip) {
2398        if (chip.isSelected()) {
2399            clearSelectedChip();
2400        }
2401    }
2402
2403    private boolean chipsPending() {
2404        return mPendingChipsCount > 0 || (mRemovedSpans != null && mRemovedSpans.size() > 0);
2405    }
2406
2407    @Override
2408    public void removeTextChangedListener(TextWatcher watcher) {
2409        mTextWatcher = null;
2410        super.removeTextChangedListener(watcher);
2411    }
2412
2413    private boolean isValidEmailAddress(String input) {
2414        return !TextUtils.isEmpty(input) && mValidator != null &&
2415                mValidator.isValid(input);
2416    }
2417
2418    private class RecipientTextWatcher implements TextWatcher {
2419
2420        @Override
2421        public void afterTextChanged(Editable s) {
2422            // If the text has been set to null or empty, make sure we remove
2423            // all the spans we applied.
2424            if (TextUtils.isEmpty(s)) {
2425                // Remove all the chips spans.
2426                Spannable spannable = getSpannable();
2427                DrawableRecipientChip[] chips = spannable.getSpans(0, getText().length(),
2428                        DrawableRecipientChip.class);
2429                for (DrawableRecipientChip chip : chips) {
2430                    spannable.removeSpan(chip);
2431                }
2432                if (mMoreChip != null) {
2433                    spannable.removeSpan(mMoreChip);
2434                }
2435                clearSelectedChip();
2436                return;
2437            }
2438            // Get whether there are any recipients pending addition to the
2439            // view. If there are, don't do anything in the text watcher.
2440            if (chipsPending()) {
2441                return;
2442            }
2443            // If the user is editing a chip, don't clear it.
2444            if (mSelectedChip != null) {
2445                if (!isGeneratedContact(mSelectedChip)) {
2446                    setCursorVisible(true);
2447                    setSelection(getText().length());
2448                    clearSelectedChip();
2449                } else {
2450                    return;
2451                }
2452            }
2453            int length = s.length();
2454            // Make sure there is content there to parse and that it is
2455            // not just the commit character.
2456            if (length > 1) {
2457                if (lastCharacterIsCommitCharacter(s)) {
2458                    commitByCharacter();
2459                    return;
2460                }
2461                char last;
2462                int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
2463                int len = length() - 1;
2464                if (end != len) {
2465                    last = s.charAt(end);
2466                } else {
2467                    last = s.charAt(len);
2468                }
2469                if (last == COMMIT_CHAR_SPACE) {
2470                    if (!isPhoneQuery()) {
2471                        // Check if this is a valid email address. If it is,
2472                        // commit it.
2473                        String text = getText().toString();
2474                        int tokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
2475                        String sub = text.substring(tokenStart, mTokenizer.findTokenEnd(text,
2476                                tokenStart));
2477                        if (isValidEmailAddress(sub)) {
2478                            commitByCharacter();
2479                        }
2480                    }
2481                }
2482            }
2483        }
2484
2485        @Override
2486        public void onTextChanged(CharSequence s, int start, int before, int count) {
2487            // The user deleted some text OR some text was replaced; check to
2488            // see if the insertion point is on a space
2489            // following a chip.
2490            if (before - count == 1) {
2491                // If the item deleted is a space, and the thing before the
2492                // space is a chip, delete the entire span.
2493                int selStart = getSelectionStart();
2494                DrawableRecipientChip[] repl = getSpannable().getSpans(selStart, selStart,
2495                        DrawableRecipientChip.class);
2496                if (repl.length > 0) {
2497                    // There is a chip there! Just remove it.
2498                    Editable editable = getText();
2499                    // Add the separator token.
2500                    int tokenStart = mTokenizer.findTokenStart(editable, selStart);
2501                    int tokenEnd = mTokenizer.findTokenEnd(editable, tokenStart);
2502                    tokenEnd = tokenEnd + 1;
2503                    if (tokenEnd > editable.length()) {
2504                        tokenEnd = editable.length();
2505                    }
2506                    editable.delete(tokenStart, tokenEnd);
2507                    getSpannable().removeSpan(repl[0]);
2508                }
2509            } else if (count > before) {
2510                if (mSelectedChip != null
2511                    && isGeneratedContact(mSelectedChip)) {
2512                    if (lastCharacterIsCommitCharacter(s)) {
2513                        commitByCharacter();
2514                        return;
2515                    }
2516                }
2517            }
2518        }
2519
2520        @Override
2521        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2522            // Do nothing.
2523        }
2524    }
2525
2526   public boolean lastCharacterIsCommitCharacter(CharSequence s) {
2527        char last;
2528        int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
2529        int len = length() - 1;
2530        if (end != len) {
2531            last = s.charAt(end);
2532        } else {
2533            last = s.charAt(len);
2534        }
2535        return last == COMMIT_CHAR_COMMA || last == COMMIT_CHAR_SEMICOLON;
2536    }
2537
2538    public boolean isGeneratedContact(DrawableRecipientChip chip) {
2539        long contactId = chip.getContactId();
2540        return contactId == RecipientEntry.INVALID_CONTACT
2541                || (!isPhoneQuery() && contactId == RecipientEntry.GENERATED_CONTACT);
2542    }
2543
2544    /**
2545     * Handles pasting a {@link ClipData} to this {@link RecipientEditTextView}.
2546     */
2547    // Visible for testing.
2548    void handlePasteClip(ClipData clip) {
2549        if (clip == null) {
2550            // Do nothing.
2551            return;
2552        }
2553
2554        final ClipDescription clipDesc = clip.getDescription();
2555        boolean containsSupportedType = clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ||
2556                clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
2557        if (!containsSupportedType) {
2558            return;
2559        }
2560
2561        removeTextChangedListener(mTextWatcher);
2562
2563        final ClipDescription clipDescription = clip.getDescription();
2564        for (int i = 0; i < clip.getItemCount(); i++) {
2565            final String mimeType = clipDescription.getMimeType(i);
2566            final boolean supportedType = ClipDescription.MIMETYPE_TEXT_PLAIN.equals(mimeType) ||
2567                    ClipDescription.MIMETYPE_TEXT_HTML.equals(mimeType);
2568            if (!supportedType) {
2569                // Only plain text and html can be pasted.
2570                continue;
2571            }
2572
2573            final CharSequence pastedItem = clip.getItemAt(i).getText();
2574            if (!TextUtils.isEmpty(pastedItem)) {
2575                final Editable editable = getText();
2576                final int start = getSelectionStart();
2577                final int end = getSelectionEnd();
2578                if (start < 0 || end < 1) {
2579                    // No selection.
2580                    editable.append(pastedItem);
2581                } else if (start == end) {
2582                    // Insert at position.
2583                    editable.insert(start, pastedItem);
2584                } else {
2585                    editable.append(pastedItem, start, end);
2586                }
2587                handlePasteAndReplace();
2588            }
2589        }
2590
2591        mHandler.post(mAddTextWatcher);
2592    }
2593
2594    @Override
2595    public boolean onTextContextMenuItem(int id) {
2596        if (id == android.R.id.paste) {
2597            ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
2598                    Context.CLIPBOARD_SERVICE);
2599            handlePasteClip(clipboard.getPrimaryClip());
2600            return true;
2601        }
2602        return super.onTextContextMenuItem(id);
2603    }
2604
2605    private void handlePasteAndReplace() {
2606        ArrayList<DrawableRecipientChip> created = handlePaste();
2607        if (created != null && created.size() > 0) {
2608            // Perform reverse lookups on the pasted contacts.
2609            IndividualReplacementTask replace = new IndividualReplacementTask();
2610            replace.execute(created);
2611        }
2612    }
2613
2614    // Visible for testing.
2615    /* package */ArrayList<DrawableRecipientChip> handlePaste() {
2616        String text = getText().toString();
2617        int originalTokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
2618        String lastAddress = text.substring(originalTokenStart);
2619        int tokenStart = originalTokenStart;
2620        int prevTokenStart = 0;
2621        DrawableRecipientChip findChip = null;
2622        ArrayList<DrawableRecipientChip> created = new ArrayList<DrawableRecipientChip>();
2623        if (tokenStart != 0) {
2624            // There are things before this!
2625            while (tokenStart != 0 && findChip == null && tokenStart != prevTokenStart) {
2626                prevTokenStart = tokenStart;
2627                tokenStart = mTokenizer.findTokenStart(text, tokenStart);
2628                findChip = findChip(tokenStart);
2629                if (tokenStart == originalTokenStart && findChip == null) {
2630                    break;
2631                }
2632            }
2633            if (tokenStart != originalTokenStart) {
2634                if (findChip != null) {
2635                    tokenStart = prevTokenStart;
2636                }
2637                int tokenEnd;
2638                DrawableRecipientChip createdChip;
2639                while (tokenStart < originalTokenStart) {
2640                    tokenEnd = movePastTerminators(mTokenizer.findTokenEnd(getText().toString(),
2641                            tokenStart));
2642                    commitChip(tokenStart, tokenEnd, getText());
2643                    createdChip = findChip(tokenStart);
2644                    if (createdChip == null) {
2645                        break;
2646                    }
2647                    // +1 for the space at the end.
2648                    tokenStart = getSpannable().getSpanEnd(createdChip) + 1;
2649                    created.add(createdChip);
2650                }
2651            }
2652        }
2653        // Take a look at the last token. If the token has been completed with a
2654        // commit character, create a chip.
2655        if (isCompletedToken(lastAddress)) {
2656            Editable editable = getText();
2657            tokenStart = editable.toString().indexOf(lastAddress, originalTokenStart);
2658            commitChip(tokenStart, editable.length(), editable);
2659            created.add(findChip(tokenStart));
2660        }
2661        return created;
2662    }
2663
2664    // Visible for testing.
2665    /* package */int movePastTerminators(int tokenEnd) {
2666        if (tokenEnd >= length()) {
2667            return tokenEnd;
2668        }
2669        char atEnd = getText().toString().charAt(tokenEnd);
2670        if (atEnd == COMMIT_CHAR_COMMA || atEnd == COMMIT_CHAR_SEMICOLON) {
2671            tokenEnd++;
2672        }
2673        // This token had not only an end token character, but also a space
2674        // separating it from the next token.
2675        if (tokenEnd < length() && getText().toString().charAt(tokenEnd) == ' ') {
2676            tokenEnd++;
2677        }
2678        return tokenEnd;
2679    }
2680
2681    private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> {
2682        private DrawableRecipientChip createFreeChip(RecipientEntry entry) {
2683            try {
2684                if (mNoChips) {
2685                    return null;
2686                }
2687                return constructChipSpan(entry, false);
2688            } catch (NullPointerException e) {
2689                Log.e(TAG, e.getMessage(), e);
2690                return null;
2691            }
2692        }
2693
2694        @Override
2695        protected void onPreExecute() {
2696            // Ensure everything is in chip-form already, so we don't have text that slowly gets
2697            // replaced
2698            final List<DrawableRecipientChip> originalRecipients =
2699                    new ArrayList<DrawableRecipientChip>();
2700            final DrawableRecipientChip[] existingChips = getSortedRecipients();
2701            for (int i = 0; i < existingChips.length; i++) {
2702                originalRecipients.add(existingChips[i]);
2703            }
2704            if (mRemovedSpans != null) {
2705                originalRecipients.addAll(mRemovedSpans);
2706            }
2707
2708            final List<DrawableRecipientChip> replacements =
2709                    new ArrayList<DrawableRecipientChip>(originalRecipients.size());
2710
2711            for (final DrawableRecipientChip chip : originalRecipients) {
2712                if (RecipientEntry.isCreatedRecipient(chip.getEntry().getContactId())
2713                        && getSpannable().getSpanStart(chip) != -1) {
2714                    replacements.add(createFreeChip(chip.getEntry()));
2715                } else {
2716                    replacements.add(null);
2717                }
2718            }
2719
2720            processReplacements(originalRecipients, replacements);
2721        }
2722
2723        @Override
2724        protected Void doInBackground(Void... params) {
2725            if (mIndividualReplacements != null) {
2726                mIndividualReplacements.cancel(true);
2727            }
2728            // For each chip in the list, look up the matching contact.
2729            // If there is a match, replace that chip with the matching
2730            // chip.
2731            final ArrayList<DrawableRecipientChip> recipients =
2732                    new ArrayList<DrawableRecipientChip>();
2733            DrawableRecipientChip[] existingChips = getSortedRecipients();
2734            for (int i = 0; i < existingChips.length; i++) {
2735                recipients.add(existingChips[i]);
2736            }
2737            if (mRemovedSpans != null) {
2738                recipients.addAll(mRemovedSpans);
2739            }
2740            ArrayList<String> addresses = new ArrayList<String>();
2741            DrawableRecipientChip chip;
2742            for (int i = 0; i < recipients.size(); i++) {
2743                chip = recipients.get(i);
2744                if (chip != null) {
2745                    addresses.add(createAddressText(chip.getEntry()));
2746                }
2747            }
2748            final BaseRecipientAdapter adapter = getAdapter();
2749            adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
2750                        @Override
2751                        public void matchesFound(Map<String, RecipientEntry> entries) {
2752                            final ArrayList<DrawableRecipientChip> replacements =
2753                                    new ArrayList<DrawableRecipientChip>();
2754                            for (final DrawableRecipientChip temp : recipients) {
2755                                RecipientEntry entry = null;
2756                                if (temp != null && RecipientEntry.isCreatedRecipient(
2757                                        temp.getEntry().getContactId())
2758                                        && getSpannable().getSpanStart(temp) != -1) {
2759                                    // Replace this.
2760                                    entry = createValidatedEntry(
2761                                            entries.get(tokenizeAddress(temp.getEntry()
2762                                                    .getDestination())));
2763                                }
2764                                if (entry != null) {
2765                                    replacements.add(createFreeChip(entry));
2766                                } else {
2767                                    replacements.add(null);
2768                                }
2769                            }
2770                            processReplacements(recipients, replacements);
2771                        }
2772
2773                        @Override
2774                        public void matchesNotFound(final Set<String> unfoundAddresses) {
2775                            final List<DrawableRecipientChip> replacements =
2776                                    new ArrayList<DrawableRecipientChip>(unfoundAddresses.size());
2777
2778                            for (final DrawableRecipientChip temp : recipients) {
2779                                if (temp != null && RecipientEntry.isCreatedRecipient(
2780                                        temp.getEntry().getContactId())
2781                                        && getSpannable().getSpanStart(temp) != -1) {
2782                                    if (unfoundAddresses.contains(
2783                                            temp.getEntry().getDestination())) {
2784                                        replacements.add(createFreeChip(temp.getEntry()));
2785                                    } else {
2786                                        replacements.add(null);
2787                                    }
2788                                } else {
2789                                    replacements.add(null);
2790                                }
2791                            }
2792
2793                            processReplacements(recipients, replacements);
2794                        }
2795                    });
2796            return null;
2797        }
2798
2799        private void processReplacements(final List<DrawableRecipientChip> recipients,
2800                final List<DrawableRecipientChip> replacements) {
2801            if (replacements != null && replacements.size() > 0) {
2802                final Runnable runnable = new Runnable() {
2803                    @Override
2804                    public void run() {
2805                        final Editable text = new SpannableStringBuilder(getText());
2806                        int i = 0;
2807                        for (final DrawableRecipientChip chip : recipients) {
2808                            final DrawableRecipientChip replacement = replacements.get(i);
2809                            if (replacement != null) {
2810                                final RecipientEntry oldEntry = chip.getEntry();
2811                                final RecipientEntry newEntry = replacement.getEntry();
2812                                final boolean isBetter =
2813                                        RecipientAlternatesAdapter.getBetterRecipient(
2814                                                oldEntry, newEntry) == newEntry;
2815
2816                                if (isBetter) {
2817                                    // Find the location of the chip in the text currently shown.
2818                                    final int start = text.getSpanStart(chip);
2819                                    if (start != -1) {
2820                                        // Replacing the entirety of what the chip represented,
2821                                        // including the extra space dividing it from other chips.
2822                                        final int end =
2823                                                Math.min(text.getSpanEnd(chip) + 1, text.length());
2824                                        text.removeSpan(chip);
2825                                        // Make sure we always have just 1 space at the end to
2826                                        // separate this chip from the next chip.
2827                                        final SpannableString displayText =
2828                                                new SpannableString(createAddressText(
2829                                                        replacement.getEntry()).trim() + " ");
2830                                        displayText.setSpan(replacement, 0,
2831                                                displayText.length() - 1,
2832                                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
2833                                        // Replace the old text we found with with the new display
2834                                        // text, which now may also contain the display name of the
2835                                        // recipient.
2836                                        text.replace(start, end, displayText);
2837                                        replacement.setOriginalText(displayText.toString());
2838                                        replacements.set(i, null);
2839
2840                                        recipients.set(i, replacement);
2841                                    }
2842                                }
2843                            }
2844                            i++;
2845                        }
2846                        setText(text);
2847                    }
2848                };
2849
2850                if (Looper.myLooper() == Looper.getMainLooper()) {
2851                    runnable.run();
2852                } else {
2853                    mHandler.post(runnable);
2854                }
2855            }
2856        }
2857    }
2858
2859    private class IndividualReplacementTask
2860            extends AsyncTask<ArrayList<DrawableRecipientChip>, Void, Void> {
2861        @Override
2862        protected Void doInBackground(ArrayList<DrawableRecipientChip>... params) {
2863            // For each chip in the list, look up the matching contact.
2864            // If there is a match, replace that chip with the matching
2865            // chip.
2866            final ArrayList<DrawableRecipientChip> originalRecipients = params[0];
2867            ArrayList<String> addresses = new ArrayList<String>();
2868            DrawableRecipientChip chip;
2869            for (int i = 0; i < originalRecipients.size(); i++) {
2870                chip = originalRecipients.get(i);
2871                if (chip != null) {
2872                    addresses.add(createAddressText(chip.getEntry()));
2873                }
2874            }
2875            final BaseRecipientAdapter adapter = getAdapter();
2876            adapter.getMatchingRecipients(addresses, new RecipientMatchCallback() {
2877
2878                        @Override
2879                        public void matchesFound(Map<String, RecipientEntry> entries) {
2880                            for (final DrawableRecipientChip temp : originalRecipients) {
2881                                if (RecipientEntry.isCreatedRecipient(temp.getEntry()
2882                                        .getContactId())
2883                                        && getSpannable().getSpanStart(temp) != -1) {
2884                                    // Replace this.
2885                                    final RecipientEntry entry = createValidatedEntry(entries
2886                                            .get(tokenizeAddress(temp.getEntry().getDestination())
2887                                                    .toLowerCase()));
2888                                    if (entry != null) {
2889                                        mHandler.post(new Runnable() {
2890                                            @Override
2891                                            public void run() {
2892                                                replaceChip(temp, entry);
2893                                            }
2894                                        });
2895                                    }
2896                                }
2897                            }
2898                        }
2899
2900                        @Override
2901                        public void matchesNotFound(final Set<String> unfoundAddresses) {
2902                            // No action required
2903                        }
2904                    });
2905            return null;
2906        }
2907    }
2908
2909
2910    /**
2911     * MoreImageSpan is a simple class created for tracking the existence of a
2912     * more chip across activity restarts/
2913     */
2914    private class MoreImageSpan extends ReplacementDrawableSpan {
2915        public MoreImageSpan(Drawable b) {
2916            super(b);
2917            setExtraMargin(mLineSpacingExtra);
2918        }
2919    }
2920
2921    @Override
2922    public boolean onDown(MotionEvent e) {
2923        return false;
2924    }
2925
2926    @Override
2927    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
2928        // Do nothing.
2929        return false;
2930    }
2931
2932    @Override
2933    public void onLongPress(MotionEvent event) {
2934        if (mSelectedChip != null) {
2935            return;
2936        }
2937        float x = event.getX();
2938        float y = event.getY();
2939        final int offset = putOffsetInRange(x, y);
2940        DrawableRecipientChip currentChip = findChip(offset);
2941        if (currentChip != null) {
2942            if (mDragEnabled) {
2943                // Start drag-and-drop for the selected chip.
2944                startDrag(currentChip);
2945            } else {
2946                // Copy the selected chip email address.
2947                showCopyDialog(currentChip.getEntry().getDestination());
2948            }
2949        }
2950    }
2951
2952    // The following methods are used to provide some functionality on older versions of Android
2953    // These methods were copied out of JB MR2's TextView
2954    /////////////////////////////////////////////////
2955    private int supportGetOffsetForPosition(float x, float y) {
2956        if (getLayout() == null) return -1;
2957        final int line = supportGetLineAtCoordinate(y);
2958        final int offset = supportGetOffsetAtCoordinate(line, x);
2959        return offset;
2960    }
2961
2962    private float supportConvertToLocalHorizontalCoordinate(float x) {
2963        x -= getTotalPaddingLeft();
2964        // Clamp the position to inside of the view.
2965        x = Math.max(0.0f, x);
2966        x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
2967        x += getScrollX();
2968        return x;
2969    }
2970
2971    private int supportGetLineAtCoordinate(float y) {
2972        y -= getTotalPaddingLeft();
2973        // Clamp the position to inside of the view.
2974        y = Math.max(0.0f, y);
2975        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
2976        y += getScrollY();
2977        return getLayout().getLineForVertical((int) y);
2978    }
2979
2980    private int supportGetOffsetAtCoordinate(int line, float x) {
2981        x = supportConvertToLocalHorizontalCoordinate(x);
2982        return getLayout().getOffsetForHorizontal(line, x);
2983    }
2984    /////////////////////////////////////////////////
2985
2986    /**
2987     * Enables drag-and-drop for chips.
2988     */
2989    public void enableDrag() {
2990        mDragEnabled = true;
2991    }
2992
2993    /**
2994     * Starts drag-and-drop for the selected chip.
2995     */
2996    private void startDrag(DrawableRecipientChip currentChip) {
2997        String address = currentChip.getEntry().getDestination();
2998        ClipData data = ClipData.newPlainText(address, address + COMMIT_CHAR_COMMA);
2999
3000        // Start drag mode.
3001        startDrag(data, new RecipientChipShadow(currentChip), null, 0);
3002
3003        // Remove the current chip, so drag-and-drop will result in a move.
3004        // TODO (phamm): consider readd this chip if it's dropped outside a target.
3005        removeChip(currentChip);
3006    }
3007
3008    /**
3009     * Handles drag event.
3010     */
3011    @Override
3012    public boolean onDragEvent(DragEvent event) {
3013        switch (event.getAction()) {
3014            case DragEvent.ACTION_DRAG_STARTED:
3015                // Only handle plain text drag and drop.
3016                return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
3017            case DragEvent.ACTION_DRAG_ENTERED:
3018                requestFocus();
3019                return true;
3020            case DragEvent.ACTION_DROP:
3021                handlePasteClip(event.getClipData());
3022                return true;
3023        }
3024        return false;
3025    }
3026
3027    /**
3028     * Drag shadow for a {@link DrawableRecipientChip}.
3029     */
3030    private final class RecipientChipShadow extends DragShadowBuilder {
3031        private final DrawableRecipientChip mChip;
3032
3033        public RecipientChipShadow(DrawableRecipientChip chip) {
3034            mChip = chip;
3035        }
3036
3037        @Override
3038        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
3039            Rect rect = mChip.getBounds();
3040            shadowSize.set(rect.width(), rect.height());
3041            shadowTouchPoint.set(rect.centerX(), rect.centerY());
3042        }
3043
3044        @Override
3045        public void onDrawShadow(Canvas canvas) {
3046            mChip.draw(canvas);
3047        }
3048    }
3049
3050    private void showCopyDialog(final String address) {
3051        if (!mAttachedToWindow) {
3052            return;
3053        }
3054        mCopyAddress = address;
3055        mCopyDialog.setTitle(address);
3056        mCopyDialog.setContentView(R.layout.copy_chip_dialog_layout);
3057        mCopyDialog.setCancelable(true);
3058        mCopyDialog.setCanceledOnTouchOutside(true);
3059        Button button = (Button)mCopyDialog.findViewById(android.R.id.button1);
3060        button.setOnClickListener(this);
3061        int btnTitleId;
3062        if (isPhoneQuery()) {
3063            btnTitleId = R.string.copy_number;
3064        } else {
3065            btnTitleId = R.string.copy_email;
3066        }
3067        String buttonTitle = getContext().getResources().getString(btnTitleId);
3068        button.setText(buttonTitle);
3069        mCopyDialog.setOnDismissListener(this);
3070        mCopyDialog.show();
3071    }
3072
3073    @Override
3074    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
3075        // Do nothing.
3076        return false;
3077    }
3078
3079    @Override
3080    public void onShowPress(MotionEvent e) {
3081        // Do nothing.
3082    }
3083
3084    @Override
3085    public boolean onSingleTapUp(MotionEvent e) {
3086        // Do nothing.
3087        return false;
3088    }
3089
3090    @Override
3091    public void onDismiss(DialogInterface dialog) {
3092        mCopyAddress = null;
3093    }
3094
3095    @Override
3096    public void onClick(View v) {
3097        // Copy this to the clipboard.
3098        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
3099                Context.CLIPBOARD_SERVICE);
3100        clipboard.setPrimaryClip(ClipData.newPlainText("", mCopyAddress));
3101        mCopyDialog.dismiss();
3102    }
3103
3104    protected boolean isPhoneQuery() {
3105        return getAdapter() != null
3106                && getAdapter().getQueryType() == BaseRecipientAdapter.QUERY_TYPE_PHONE;
3107    }
3108
3109    @Override
3110    public BaseRecipientAdapter getAdapter() {
3111        return (BaseRecipientAdapter) super.getAdapter();
3112    }
3113
3114    /**
3115     * Append a new {@link RecipientEntry} to the end of the recipient chips, leaving any
3116     * unfinished text at the end.
3117     */
3118    public void appendRecipientEntry(final RecipientEntry entry) {
3119        clearComposingText();
3120
3121        final Editable editable = getText();
3122        int chipInsertionPoint = 0;
3123
3124        // Find the end of last chip and see if there's any unchipified text.
3125        final DrawableRecipientChip[] recips = getSortedRecipients();
3126        if (recips != null && recips.length > 0) {
3127            final DrawableRecipientChip last = recips[recips.length - 1];
3128            // The chip will be inserted at the end of last chip + 1. All the unfinished text after
3129            // the insertion point will be kept untouched.
3130            chipInsertionPoint = editable.getSpanEnd(last) + 1;
3131        }
3132
3133        final CharSequence chip = createChip(entry, false);
3134        if (chip != null) {
3135            editable.insert(chipInsertionPoint, chip);
3136        }
3137    }
3138
3139    /**
3140     * Remove all chips matching the given RecipientEntry.
3141     */
3142    public void removeRecipientEntry(final RecipientEntry entry) {
3143        final DrawableRecipientChip[] recips = getText()
3144                .getSpans(0, getText().length(), DrawableRecipientChip.class);
3145
3146        for (final DrawableRecipientChip recipient : recips) {
3147            final RecipientEntry existingEntry = recipient.getEntry();
3148            if (existingEntry != null && existingEntry.isValid() &&
3149                    existingEntry.isSamePerson(entry)) {
3150                removeChip(recipient);
3151            }
3152        }
3153    }
3154
3155    public void setAlternatePopupAnchor(View v) {
3156        mAlternatePopupAnchor = v;
3157    }
3158
3159    private static class ChipBitmapContainer {
3160        Bitmap bitmap;
3161        // information used for positioning the loaded icon
3162        boolean loadIcon = true;
3163        float left;
3164        float top;
3165        float right;
3166        float bottom;
3167    }
3168}
3169