RecipientEditTextView.java revision 5a197cb9af43e1db6b882d89c23949577ceba9a4
1fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi/*
2fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * Copyright (C) 2011 The Android Open Source Project
3fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi *
4fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * Licensed under the Apache License, Version 2.0 (the "License");
5fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * you may not use this file except in compliance with the License.
6fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * You may obtain a copy of the License at
7fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi *
8fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi *      http://www.apache.org/licenses/LICENSE-2.0
9fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi *
10fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * Unless required by applicable law or agreed to in writing, software
11fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * distributed under the License is distributed on an "AS IS" BASIS,
12fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * See the License for the specific language governing permissions and
14fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi * limitations under the License.
15fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi */
16fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi
17e9236d046fdb5cac0696c42e03443a2439188146Jean-Michel Trivipackage com.android.ex.chips;
18fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi
19fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Triviimport android.app.Dialog;
204ee246c55533bdab8ab5fa0f0581744fe58e7c91Jean-Michel Triviimport android.content.ClipData;
214ee246c55533bdab8ab5fa0f0581744fe58e7c91Jean-Michel Triviimport android.content.ClipboardManager;
2263c002ab68761be0eace98f28320d8eb2f3f7695Jean-Michel Triviimport android.content.Context;
2326043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.content.DialogInterface;
244ee246c55533bdab8ab5fa0f0581744fe58e7c91Jean-Michel Triviimport android.content.DialogInterface.OnDismissListener;
25a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.graphics.Bitmap;
26fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Triviimport android.graphics.BitmapFactory;
27665ca3f1b0fc90cd5980a435d164354b2529c0b5Andreas Huberimport android.graphics.Canvas;
2826043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.graphics.Matrix;
29eae4df541ba1d46f65d37e959baf2127aa632c93Jean-Michel Triviimport android.graphics.Rect;
30fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Triviimport android.graphics.RectF;
31fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Triviimport android.graphics.drawable.BitmapDrawable;
32be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Triviimport android.graphics.drawable.Drawable;
337133228a478e16458b659946f2180ecddd13fda7Glenn Kastenimport android.os.AsyncTask;
347133228a478e16458b659946f2180ecddd13fda7Glenn Kastenimport android.os.Handler;
35a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.os.Message;
36a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.text.Editable;
37a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.text.InputType;
38a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.text.Layout;
39a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.text.Spannable;
40a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.text.SpannableString;
417133228a478e16458b659946f2180ecddd13fda7Glenn Kastenimport android.text.SpannableStringBuilder;
42bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Triviimport android.text.Spanned;
43bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Triviimport android.text.TextPaint;
4426043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.text.TextUtils;
45ecc4fe22e076c4e5c891d823b01db1a683ba6690Glenn Kastenimport android.text.TextWatcher;
4626043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.text.method.QwertyKeyListener;
4726043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.text.style.ImageSpan;
4826043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.text.util.Rfc822Token;
49513222822545c3e954176476b263df52a47f43a4Glenn Kastenimport android.text.util.Rfc822Tokenizer;
50a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.util.AttributeSet;
5126043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.util.Log;
5226043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.view.ActionMode;
531c853a41d9d9886e60618a7c878ce3912f46bf3cJean-Michel Triviimport android.view.ActionMode.Callback;
541c853a41d9d9886e60618a7c878ce3912f46bf3cJean-Michel Triviimport android.view.GestureDetector;
551c853a41d9d9886e60618a7c878ce3912f46bf3cJean-Michel Triviimport android.view.KeyEvent;
561c853a41d9d9886e60618a7c878ce3912f46bf3cJean-Michel Triviimport android.view.LayoutInflater;
571c853a41d9d9886e60618a7c878ce3912f46bf3cJean-Michel Triviimport android.view.Menu;
581c853a41d9d9886e60618a7c878ce3912f46bf3cJean-Michel Triviimport android.view.MenuItem;
5926043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.view.MotionEvent;
6026043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.view.View;
6126043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.view.ViewGroup;
62a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.view.View.OnClickListener;
6370c49ae2867094072a4365423417ea452bf82231Jean-Michel Triviimport android.view.ViewParent;
64a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.widget.AdapterView;
6526043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.widget.AdapterView.OnItemClickListener;
6626043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.widget.Filterable;
6726043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.widget.ListAdapter;
6826043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.widget.ListPopupWindow;
69a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.widget.ListView;
70a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.widget.MultiAutoCompleteTextView;
7126043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport android.widget.ScrollView;
72a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenimport android.widget.TextView;
7326043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Trivi
7426043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport java.util.ArrayList;
7526043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport java.util.Arrays;
7670c49ae2867094072a4365423417ea452bf82231Jean-Michel Triviimport java.util.Collection;
7726043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Triviimport java.util.Collections;
7870c49ae2867094072a4365423417ea452bf82231Jean-Michel Triviimport java.util.Comparator;
7970c49ae2867094072a4365423417ea452bf82231Jean-Michel Triviimport java.util.HashMap;
804a4c728965b10df683d37b4b7ac27211e02006adJean-Michel Triviimport java.util.HashSet;
814a4c728965b10df683d37b4b7ac27211e02006adJean-Michel Triviimport java.util.Set;
824a4c728965b10df683d37b4b7ac27211e02006adJean-Michel Trivi
834a4c728965b10df683d37b4b7ac27211e02006adJean-Michel Trivi/**
84a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten * RecipientEditTextView is an auto complete text view for use with applications
85a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten * that use the new Chips UI for addressing a message to recipients.
86a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten */
87a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kastenpublic class RecipientEditTextView extends MultiAutoCompleteTextView implements
88a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        OnItemClickListener, Callback, RecipientAlternatesAdapter.OnCheckedItemChangedListener,
8970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        GestureDetector.OnGestureListener, OnDismissListener, OnClickListener {
90e2e8fa36bd7448b59fbcdf141e0b6d21e5401d91Glenn Kasten
91e2e8fa36bd7448b59fbcdf141e0b6d21e5401d91Glenn Kasten    private static final String TAG = "RecipientEditTextView";
9270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
9370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    // TODO: get correct number/ algorithm from with UX.
9470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private static final int CHIP_LIMIT = 2;
9570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
9670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private Drawable mChipBackground = null;
9770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
9870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private Drawable mChipDelete = null;
99be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi
10070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private int mChipPadding;
10126043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Trivi
10226043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Trivi    private Tokenizer mTokenizer;
10326043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Trivi
10470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private Drawable mChipBackgroundPressed;
105be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi
106be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi    private RecipientChip mSelectedChip;
107be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi
108be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi    private int mAlternatesLayout;
109be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi
110a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private Bitmap mDefaultContactPhoto;
111a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
112a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private ImageSpan mMoreChip;
113a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
114a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private TextView mMoreItem;
115a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
116a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private final ArrayList<String> mPendingChips = new ArrayList<String>();
117a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
118a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private float mChipHeight;
119a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
120a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private float mChipFontSize;
121a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
12270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private Validator mValidator;
123bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi
12470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private Drawable mInvalidChipBackground;
12570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
126485a038f9f0f898227b8ab4218e94c5d56b6ed0bGlenn Kasten    private Handler mHandler;
127485a038f9f0f898227b8ab4218e94c5d56b6ed0bGlenn Kasten
12870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private static int DISMISS = "dismiss".hashCode();
12970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
13070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private static final long DISMISS_DELAY = 300;
13170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
13270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private int mPendingChipsCount = 0;
133677c76347d9aaca4cf3746b3dbfc8a741281066bGlenn Kasten
13470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private static int sSelectedTextColor = -1;
135677c76347d9aaca4cf3746b3dbfc8a741281066bGlenn Kasten
13670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private static final char COMMIT_CHAR_COMMA = ',';
13770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
13870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private static final char COMMIT_CHAR_SEMICOLON = ';';
13970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
1406bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi    private static final char COMMIT_CHAR_SPACE = ' ';
1416bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi
1426bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi    private ListPopupWindow mAlternatesPopup;
1436bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi
14470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private ListPopupWindow mAddressPopup;
145a8179ea15c4ff78db589d742b135649f0eda7ef2Glenn Kasten
14670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private ArrayList<RecipientChip> mTemporaryRecipients;
14770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
14870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private ArrayList<RecipientChip> mRemovedSpans;
14970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
15070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private boolean mShouldShrink = true;
15170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
15270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    // Chip copy fields.
153677c76347d9aaca4cf3746b3dbfc8a741281066bGlenn Kasten    private GestureDetector mGestureDetector;
15470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
15570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private Dialog mCopyDialog;
1566bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi
1576bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi    private int mCopyViewRes;
15870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
15970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    private String mCopyAddress;
16070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
16170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    /**
16270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi     * Used with {@link #mAlternatesPopup}. Handles clicks to alternate addresses for a
16370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi     * selected chip.
16470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi     */
165af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi    private OnItemClickListener mAlternatesListener;
166af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi
1676f0f5640d190b0187c356eb53bd96d9f9e49da60Jean-Michel Trivi    private int mCheckedItem;
168665ca3f1b0fc90cd5980a435d164354b2529c0b5Andreas Huber    private TextWatcher mTextWatcher;
169665ca3f1b0fc90cd5980a435d164354b2529c0b5Andreas Huber
170665ca3f1b0fc90cd5980a435d164354b2529c0b5Andreas Huber    private ScrollView mScrollView;
171af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi
172af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi    private boolean mTried;
173af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi
174af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi    private final Runnable mAddTextWatcher = new Runnable() {
175af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi        @Override
176af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi        public void run() {
177af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi            if (mTextWatcher == null) {
1786f0f5640d190b0187c356eb53bd96d9f9e49da60Jean-Michel Trivi                mTextWatcher = new RecipientTextWatcher();
17970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                addTextChangedListener(mTextWatcher);
180af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi            }
181af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi        }
182af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi    };
183af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi
184a9f22e6f5f53e90daa779e38b22f88e4faa35c95Glenn Kasten    private IndividualReplacementTask mIndividualReplacements;
185af9b87de97356722370d11d2c5797d75cb43969eJean-Michel Trivi
186a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private Runnable mHandlePendingChips = new Runnable() {
187a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
188a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        @Override
189a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        public void run() {
190a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten            handlePendingChips();
191a9f22e6f5f53e90daa779e38b22f88e4faa35c95Glenn Kasten        }
19270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
19370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    };
19470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
19570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    public RecipientEditTextView(Context context, AttributeSet attrs) {
19670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        super(context, attrs);
19770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        if (sSelectedTextColor == -1) {
19870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            sSelectedTextColor = context.getResources().getColor(android.R.color.white);
19970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        }
20070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        mAlternatesPopup = new ListPopupWindow(context);
20170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        mAddressPopup = new ListPopupWindow(context);
20270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        mCopyDialog = new Dialog(context);
20370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        mAlternatesListener = new OnItemClickListener() {
20470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            @Override
20570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            public void onItemClick(AdapterView<?> adapterView,View view, int position,
20670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                    long rowId) {
20770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                mAlternatesPopup.setOnItemClickListener(null);
20870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                replaceChip(mSelectedChip, ((RecipientAlternatesAdapter) adapterView.getAdapter())
20970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                        .getRecipientEntry(position));
21070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                Message delayed = Message.obtain(mHandler, DISMISS);
21170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                delayed.obj = mAlternatesPopup;
21270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                mHandler.sendMessageDelayed(delayed, DISMISS_DELAY);
21370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                clearComposingText();
21470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            }
21570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        };
21670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
21770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        setOnItemClickListener(this);
21870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        setCustomSelectionActionModeCallback(this);
219a8179ea15c4ff78db589d742b135649f0eda7ef2Glenn Kasten        mHandler = new Handler() {
22070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            @Override
22170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            public void handleMessage(Message msg) {
22270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                if (msg.what == DISMISS) {
22370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                    ((ListPopupWindow) msg.obj).dismiss();
22470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                    return;
22570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                }
22670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                super.handleMessage(msg);
22770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            }
22870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        };
22970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        mTextWatcher = new RecipientTextWatcher();
23070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        addTextChangedListener(mTextWatcher);
23170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        mGestureDetector = new GestureDetector(context, this);
23270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    }
23370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
23470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    @Override
23570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
23670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        super.setAdapter(adapter);
23770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        if (adapter == null) {
23870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            return;
23970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        }
24070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    }
24170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi
24270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    @Override
24370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi    public void onSelectionChanged(int start, int end) {
2446bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        // When selection changes, see if it is inside the chips area.
24570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        // If so, move the cursor back after the chips again.
2466bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        Spannable span = getSpannable();
2476bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        int textLength = getText().length();
2486bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        RecipientChip[] chips = span.getSpans(start, textLength, RecipientChip.class);
2496bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        if (chips != null && chips.length > 0) {
2506bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi            if (chips != null && chips.length > 0) {
2516bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi                // Grab the last chip and set the cursor to after it.
2526bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi                setSelection(Math.min(span.getSpanEnd(chips[chips.length - 1]) + 1, textLength));
2536bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi            }
2546bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        }
2556bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        super.onSelectionChanged(start, end);
2566bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi    }
2576bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi
2586bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi    /**
259bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi     * Convenience method: Append the specified text slice to the TextView's
26070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi     * display buffer, upgrading it to BufferType.EDITABLE if it was
2616bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi     * not already editable. Commas are excluded as they are added automatically
2626bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi     * by the view.
2636bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi     */
2646bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi    @Override
265a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    public void append(CharSequence text, int start, int end) {
266a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        // We don't care about watching text changes while appending.
267a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        if (mTextWatcher != null) {
268a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten            removeTextChangedListener(mTextWatcher);
2696bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi        }
27070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        super.append(text, start, end);
27170c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        if (!TextUtils.isEmpty(text) && TextUtils.getTrimmedLength(text) > 0) {
27270c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            final String displayString = (String) text;
27370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            int seperatorPos = displayString.indexOf(COMMIT_CHAR_COMMA);
2746bc8af4e67051af7c86c311cb9c50e294e547500Jean-Michel Trivi            if (seperatorPos != 0 && !TextUtils.isEmpty(displayString)
27570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                    && TextUtils.getTrimmedLength(displayString) > 0) {
27670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                mPendingChipsCount++;
27770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                mPendingChips.add((String)text);
278a07e6cd61b12a5c6ed78adaa88a08abd028f5a64Jean-Michel Trivi            }
27970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        }
28057ec2b5fbd0c7c08f068e1f7b9d7644b0932617bGlenn Kasten        // Put a message on the queue to make sure we ALWAYS handle pending chips.
281677c76347d9aaca4cf3746b3dbfc8a741281066bGlenn Kasten        if (mPendingChipsCount > 0) {
282677c76347d9aaca4cf3746b3dbfc8a741281066bGlenn Kasten            postHandlePendingChips();
2831c853a41d9d9886e60618a7c878ce3912f46bf3cJean-Michel Trivi        }
284677c76347d9aaca4cf3746b3dbfc8a741281066bGlenn Kasten        mHandler.post(mAddTextWatcher);
285677c76347d9aaca4cf3746b3dbfc8a741281066bGlenn Kasten    }
28657ec2b5fbd0c7c08f068e1f7b9d7644b0932617bGlenn Kasten
28757ec2b5fbd0c7c08f068e1f7b9d7644b0932617bGlenn Kasten    @Override
28857ec2b5fbd0c7c08f068e1f7b9d7644b0932617bGlenn Kasten    public void onFocusChanged(boolean hasFocus, int direction, Rect previous) {
28957ec2b5fbd0c7c08f068e1f7b9d7644b0932617bGlenn Kasten        super.onFocusChanged(hasFocus, direction, previous);
29070c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        if (!hasFocus) {
291485a038f9f0f898227b8ab4218e94c5d56b6ed0bGlenn Kasten            shrink();
292485a038f9f0f898227b8ab4218e94c5d56b6ed0bGlenn Kasten        } else {
293485a038f9f0f898227b8ab4218e94c5d56b6ed0bGlenn Kasten            expand();
29470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            scrollLineIntoView(getLineCount());
29570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        }
296be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi    }
29726043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Trivi
298167a2af67dcc0d20e6e3e995a23a0567715e0ee1Glenn Kasten    @Override
2997133228a478e16458b659946f2180ecddd13fda7Glenn Kasten    public void performValidation() {
30068d56b8ebaf60184a3aef988e3d2b09ed8b88c05Jean-Michel Trivi        // Do nothing. Chips handles its own validation.
301d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten    }
302d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten
303fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi    private void shrink() {
304e9236d046fdb5cac0696c42e03443a2439188146Jean-Michel Trivi        if (mSelectedChip != null
305fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi                && mSelectedChip.getEntry().getContactId() != RecipientEntry.INVALID_CONTACT) {
306fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi            clearSelectedChip();
307fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi        } else {
308e9236d046fdb5cac0696c42e03443a2439188146Jean-Michel Trivi            // Reset any pending chips as they would have been handled
309a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten            // when the field lost focus.
310fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi            if (mPendingChipsCount > 0) {
311fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi                postHandlePendingChips();
312fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi            } else {
31370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                Editable editable = getText();
31470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                int end = getSelectionEnd();
315bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi                int start = mTokenizer.findTokenStart(editable, end);
316bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi                RecipientChip[] chips = getSpannable().getSpans(start, end, RecipientChip.class);
31770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                if ((chips == null || chips.length == 0)) {
31870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                    int whatEnd = mTokenizer.findTokenEnd(getText(), start);
319d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten                    // In the middle of chip; treat this as an edit
320d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten                    // and commit the whole token.
321d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten                    if (whatEnd != getSelectionEnd()) {
322d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten                        handleEdit(start, whatEnd);
32370c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                    } else {
32470c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                        commitChip(start, end, editable);
32570c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                    }
32670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                }
32770c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            }
32870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi            mHandler.post(mAddTextWatcher);
32970c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        }
330d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        createMoreChip();
331d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten    }
332d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten
333d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten    private void expand() {
334d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        removeMoreChip();
335d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        setCursorVisible(true);
336d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        Editable text = getText();
337d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        setSelection(text != null && text.length() > 0 ? text.length() : 0);
338d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        // If there are any temporary chips, try replacing them now that the user
339a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        // has expanded the field.
340a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        if (mTemporaryRecipients != null && mTemporaryRecipients.size() > 0) {
341d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten            new RecipientReplacementTask().execute();
342d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten            mTemporaryRecipients = null;
343d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        }
344d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten    }
345d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten
346d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten    private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) {
347a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        paint.setTextSize(mChipFontSize);
348a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        if (maxWidth <= 0 && Log.isLoggable(TAG, Log.DEBUG)) {
349241b9c06493479dc632a8851097c193b724a2b41Andreas Huber            Log.d(TAG, "Max width is negative: " + maxWidth);
350a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        }
351a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        return TextUtils.ellipsize(text, paint, maxWidth,
352a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten                TextUtils.TruncateAt.END);
353a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    }
354a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten
355a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten    private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint, Layout layout) {
356a0fa47f72f47fffb80ab2ae791739ce73de1e8f4Glenn Kasten        // Ellipsize the text so that it takes AT MOST the entire width of the
357d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        // autocomplete text entry area. Make sure to leave space for padding
3588ab6c02fa1384397d24eb8c15029577b17ab7c71Glenn Kasten        // on the sides.
3598ab6c02fa1384397d24eb8c15029577b17ab7c71Glenn Kasten        int height = (int) mChipHeight;
3608ab6c02fa1384397d24eb8c15029577b17ab7c71Glenn Kasten        int deleteWidth = height;
3618ab6c02fa1384397d24eb8c15029577b17ab7c71Glenn Kasten        CharSequence ellipsizedText = ellipsizeText(contact.getDisplayName(), paint,
362d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten                calculateAvailableWidth(true) - deleteWidth);
363d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten
364d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        // Make sure there is a minimum chip width so the user can ALWAYS
365d1e9fd4cff80becfef5077090fc90328ba63999aGlenn Kasten        // tap a chip without difficulty.
36670c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        int width = Math.max(deleteWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
367bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi                ellipsizedText.length()))
36870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi                + (mChipPadding * 2) + deleteWidth);
369bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi
370bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi        // Create the background of the chip.
371bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
372bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi        Canvas canvas = new Canvas(tmpBitmap);
373be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi        if (mChipBackgroundPressed != null) {
374be59fc5cfd9354d70d4b0e28bb2bca24a6ca6f22Jean-Michel Trivi            mChipBackgroundPressed.setBounds(0, 0, width, height);
375e7bfcdc183454ec959ff51342f0973cabba219b2Jean-Michel Trivi            mChipBackgroundPressed.draw(canvas);
376e7bfcdc183454ec959ff51342f0973cabba219b2Jean-Michel Trivi            paint.setColor(sSelectedTextColor);
37737dc2fccf3f122b79ebd554de209d0a3c94ae161Jean-Michel Trivi            // Vertically center the text in the chip.
37837dc2fccf3f122b79ebd554de209d0a3c94ae161Jean-Michel Trivi            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding,
37926043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Trivi                    getTextYOffset((String) ellipsizedText, paint, height), paint);
38026043f06b7d6cb2f93a2f2e7846a4e59da722206Jean-Michel Trivi            // Make the delete a square.
381cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi            mChipDelete.setBounds(width - deleteWidth, 0, width, height);
382cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi            mChipDelete.draw(canvas);
383cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi        } else {
384cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
385e9236d046fdb5cac0696c42e03443a2439188146Jean-Michel Trivi        }
38685edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten        return tmpBitmap;
38785edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten    }
38885edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten
389e52e877354b1477d5cb34d24c70417820b013521Dave Burke
39085edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten    /**
39185edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten     * Get the background drawable for a RecipientChip.
392e52e877354b1477d5cb34d24c70417820b013521Dave Burke     */
393e52e877354b1477d5cb34d24c70417820b013521Dave Burke    public Drawable getChipBackground(RecipientEntry contact) {
394e52e877354b1477d5cb34d24c70417820b013521Dave Burke        return (mValidator != null && mValidator.isValid(contact.getDestination())) ?
39585edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten                mChipBackground : mInvalidChipBackground;
39685edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten    }
39785edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten
39885edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten    private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint, Layout layout) {
39985edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten        // Ellipsize the text so that it takes AT MOST the entire width of the
40085edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten        // autocomplete text entry area. Make sure to leave space for padding
40185edd878a30caa535b0267d8d6e61b4ccc0d5fd0Glenn Kasten        // on the sides.
402cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi        int height = (int) mChipHeight;
403cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi        int iconWidth = height;
404cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi        String displayText =
405b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi            !TextUtils.isEmpty(contact.getDisplayName()) ? contact.getDisplayName() :
406b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi            !TextUtils.isEmpty(contact.getDestination()) ? contact.getDestination() : "";
407b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi        CharSequence ellipsizedText = ellipsizeText(displayText, paint,
408b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi                calculateAvailableWidth(false) - iconWidth);
409bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi        // Make sure there is a minimum chip width so the user can ALWAYS
410b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi        // tap a chip without difficulty.
411b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi        int width = Math.max(iconWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
412b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi                ellipsizedText.length()))
413b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi                + (mChipPadding * 2) + iconWidth);
414b712aebe63a6c50cc01f4493282fc77578242976Jean-Michel Trivi
415bc0e642e6c1a51b3ae3a02d490d94b03e718e6b5Jean-Michel Trivi        // Create the background of the chip.
4167133228a478e16458b659946f2180ecddd13fda7Glenn Kasten        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
4177133228a478e16458b659946f2180ecddd13fda7Glenn Kasten        Canvas canvas = new Canvas(tmpBitmap);
41870c49ae2867094072a4365423417ea452bf82231Jean-Michel Trivi        Drawable background = getChipBackground(contact);
419cb417262919560a715628f4317dc8a4de386f750Jean-Michel Trivi        if (background != null) {
420fe6f6b9ed3683119721618e1aeaa8c7d6baee188Jean-Michel Trivi            background.setBounds(0, 0, width, height);
421            background.draw(canvas);
422
423            // Don't draw photos for recipients that have been typed in.
424            if (contact.getContactId() != RecipientEntry.INVALID_CONTACT) {
425                byte[] photoBytes = contact.getPhotoBytes();
426                // There may not be a photo yet if anything but the first contact address
427                // was selected.
428                if (photoBytes == null && contact.getPhotoThumbnailUri() != null) {
429                    // TODO: cache this in the recipient entry?
430                    ((BaseRecipientAdapter) getAdapter()).fetchPhoto(contact, contact
431                            .getPhotoThumbnailUri());
432                    photoBytes = contact.getPhotoBytes();
433                }
434
435                Bitmap photo;
436                if (photoBytes != null) {
437                    photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
438                } else {
439                    // TODO: can the scaled down default photo be cached?
440                    photo = mDefaultContactPhoto;
441                }
442                // Draw the photo on the left side.
443                Matrix matrix = new Matrix();
444                RectF src = new RectF(0, 0, photo.getWidth(), photo.getHeight());
445                RectF dst = new RectF(width - iconWidth, 0, width, height);
446                matrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
447                canvas.drawBitmap(photo, matrix, paint);
448            } else {
449                // Don't leave any space for the icon. It isn't being drawn.
450                iconWidth = 0;
451            }
452
453            // Vertically center the text in the chip.
454            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding,
455                    getTextYOffset((String)ellipsizedText, paint, height), paint);
456        } else {
457            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
458        }
459        return tmpBitmap;
460    }
461
462    private float getTextYOffset(String text, TextPaint paint, int height) {
463        Rect bounds = new Rect();
464        paint.getTextBounds((String)text, 0, text.length(), bounds);
465        int textHeight = bounds.bottom - bounds.top  - (int)paint.descent();
466        return height - ((height - textHeight) / 2);
467    }
468
469    public RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
470            throws NullPointerException {
471        if (mChipBackground == null) {
472            throw new NullPointerException(
473                    "Unable to render any chips as setChipDimensions was not called.");
474        }
475        Layout layout = getLayout();
476
477        TextPaint paint = getPaint();
478        float defaultSize = paint.getTextSize();
479        int defaultColor = paint.getColor();
480
481        Bitmap tmpBitmap;
482        if (pressed) {
483            tmpBitmap = createSelectedChip(contact, paint, layout);
484
485        } else {
486            tmpBitmap = createUnselectedChip(contact, paint, layout);
487        }
488
489        // Pass the full text, un-ellipsized, to the chip.
490        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
491        result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
492        RecipientChip recipientChip = new RecipientChip(result, contact, offset);
493        // Return text to the original size.
494        paint.setTextSize(defaultSize);
495        paint.setColor(defaultColor);
496        return recipientChip;
497    }
498
499    /**
500     * Calculate the bottom of the line the chip will be located on using:
501     * 1) which line the chip appears on
502     * 2) the height of a chip
503     * 3) padding built into the edit text view
504     */
505    private int calculateOffsetFromBottom(int line) {
506        // Line offsets start at zero.
507        int actualLine = getLineCount() - (line + 1);
508        return -((actualLine * ((int) mChipHeight) + getPaddingBottom()) + getPaddingTop())
509                + getDropDownVerticalOffset();
510    }
511
512    /**
513     * Get the max amount of space a chip can take up. The formula takes into
514     * account the width of the EditTextView, any view padding, and padding
515     * that will be added to the chip.
516     */
517    private float calculateAvailableWidth(boolean pressed) {
518        return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2);
519    }
520
521    /**
522     * Set all chip dimensions and resources. This has to be done from the
523     * application as this is a static library.
524     * @param chipBackground
525     * @param chipBackgroundPressed
526     * @param invalidChip
527     * @param chipDelete
528     * @param defaultContact
529     * @param moreResource
530     * @param alternatesLayout
531     * @param chipHeight
532     * @param padding Padding around the text in a chip
533     * @param chipFontSize
534     * @param copyViewRes
535     */
536    public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
537            Drawable invalidChip, Drawable chipDelete, Bitmap defaultContact, int moreResource,
538            int alternatesLayout, float chipHeight, float padding,
539            float chipFontSize, int copyViewRes) {
540        mChipBackground = chipBackground;
541        mChipBackgroundPressed = chipBackgroundPressed;
542        mChipDelete = chipDelete;
543        mChipPadding = (int) padding;
544        mAlternatesLayout = alternatesLayout;
545        mDefaultContactPhoto = defaultContact;
546        mMoreItem = (TextView) LayoutInflater.from(getContext()).inflate(moreResource, null);
547        mChipHeight = chipHeight;
548        mChipFontSize = chipFontSize;
549        mInvalidChipBackground = invalidChip;
550        mCopyViewRes = copyViewRes;
551    }
552
553    /**
554     * Set whether to shrink the recipients field such that at most
555     * one line of recipients chips are shown when the field loses
556     * focus. By default, the number of displayed recipients will be
557     * limited and a "more" chip will be shown when focus is lost.
558     * @param shrink
559     */
560    public void setOnFocusListShrinkRecipients(boolean shrink) {
561        mShouldShrink = shrink;
562    }
563
564    @Override
565    public void onSizeChanged(int width, int height, int oldw, int oldh) {
566        super.onSizeChanged(width, height, oldw, oldh);
567        if (width != 0 && height != 0 && mPendingChipsCount > 0) {
568            postHandlePendingChips();
569        }
570        // Try to find the scroll view parent, if it exists.
571        if (mScrollView == null && !mTried) {
572            ViewParent parent = getParent();
573            while (parent != null && !(parent instanceof ScrollView)) {
574                parent = parent.getParent();
575            }
576            if (parent != null) {
577                mScrollView = (ScrollView) parent;
578            }
579            mTried = true;
580        }
581    }
582
583    private void postHandlePendingChips() {
584        mHandler.removeCallbacks(mHandlePendingChips);
585        mHandler.post(mHandlePendingChips);
586    }
587
588    private void handlePendingChips() {
589        if (mPendingChipsCount <= 0) {
590            return;
591        }
592        if (getWidth() <= 0) {
593            // The widget has not been sized yet.
594            // This will be called as a result of onSizeChanged
595            // at a later point.
596            return;
597        }
598        synchronized (mPendingChips) {
599            mTemporaryRecipients = new ArrayList<RecipientChip>(mPendingChipsCount);
600            Editable editable = getText();
601            // Tokenize!
602            for (int i = 0; i < mPendingChips.size(); i++) {
603                String current = mPendingChips.get(i);
604                int tokenStart = editable.toString().indexOf(current);
605                int tokenEnd = tokenStart + current.length();
606                if (tokenStart >= 0) {
607                    // When we have a valid token, include it with the token
608                    // to the left.
609                    if (tokenEnd < editable.length() - 2
610                            && editable.charAt(tokenEnd) == COMMIT_CHAR_COMMA) {
611                        tokenEnd++;
612                    }
613                    createReplacementChip(tokenStart, tokenEnd, editable);
614                }
615                mPendingChipsCount--;
616            }
617            sanitizeSpannable();
618            if (mTemporaryRecipients != null
619                    && mTemporaryRecipients.size() <= RecipientAlternatesAdapter.MAX_LOOKUPS) {
620                if (hasFocus() || mTemporaryRecipients.size() < CHIP_LIMIT) {
621                    new RecipientReplacementTask().execute();
622                    mTemporaryRecipients = null;
623                } else {
624                    // Create the "more" chip
625                    mIndividualReplacements = new IndividualReplacementTask();
626                    mIndividualReplacements.execute(new ArrayList<RecipientChip>(
627                            mTemporaryRecipients.subList(0, CHIP_LIMIT)));
628
629                    createMoreChip();
630                }
631            } else {
632                // There are too many recipients to look up, so just fall back
633                // to
634                // showing addresses for all of them.
635                mTemporaryRecipients = null;
636                createMoreChip();
637            }
638            mPendingChipsCount = 0;
639            mPendingChips.clear();
640        }
641    }
642
643    /**
644     * Remove any characters after the last valid chip.
645     */
646    private void sanitizeSpannable() {
647        // Find the last chip; eliminate any commit characters after it.
648        RecipientChip[] chips = getRecipients();
649        if (chips != null && chips.length > 0) {
650            int end;
651            ImageSpan lastSpan;
652            if (mMoreChip != null) {
653                lastSpan = mMoreChip;
654            } else {
655                lastSpan = chips[chips.length - 1];
656            }
657            end = getSpannable().getSpanEnd(lastSpan);
658            Editable editable = getText();
659            int length = editable.length();
660            if (length > end) {
661                // See what characters occur after that and eliminate them.
662                if (Log.isLoggable(TAG, Log.DEBUG)) {
663                    Log.d(TAG, "There were extra characters after the last tokenizable entry."
664                            + editable);
665                }
666                editable.delete(end + 1, length);
667            }
668        }
669    }
670
671    /**
672     * Create a chip that represents just the email address of a recipient. At some later
673     * point, this chip will be attached to a real contact entry, if one exists.
674     */
675    private void createReplacementChip(int tokenStart, int tokenEnd, Editable editable) {
676        if (alreadyHasChip(tokenStart, tokenEnd)) {
677            // There is already a chip present at this location.
678            // Don't recreate it.
679            return;
680        }
681        String token = editable.toString().substring(tokenStart, tokenEnd);
682        int commitCharIndex = token.trim().lastIndexOf(COMMIT_CHAR_COMMA);
683        if (commitCharIndex == token.length() - 1) {
684            token = token.substring(0, token.length() - 1);
685        }
686        RecipientEntry entry = createTokenizedEntry(token);
687        if (entry != null) {
688            String destText = entry.getDestination();
689            destText = (String) mTokenizer.terminateToken(destText);
690            // Always leave a blank space at the end of a chip.
691            int textLength = destText.length() - 1;
692            SpannableString chipText = new SpannableString(destText);
693            int end = getSelectionEnd();
694            int start = mTokenizer.findTokenStart(getText(), end);
695            RecipientChip chip = null;
696            try {
697                chip = constructChipSpan(entry, start, false);
698                chipText.setSpan(chip, 0, textLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
699            } catch (NullPointerException e) {
700                Log.e(TAG, e.getMessage(), e);
701            }
702
703            editable.replace(tokenStart, tokenEnd, chipText);
704            // Add this chip to the list of entries "to replace"
705            if (chip != null) {
706                chip.setOriginalText(chipText.toString());
707                mTemporaryRecipients.add(chip);
708            }
709        }
710    }
711
712    private RecipientEntry createTokenizedEntry(String token) {
713        if (TextUtils.isEmpty(token)) {
714            return null;
715        }
716        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(token);
717        String display = null;
718        if (isValid(token) && tokens != null && tokens.length > 0) {
719            // If we can get a name from tokenizing, then generate an entry from
720            // this.
721            display = tokens[0].getName();
722            if (!TextUtils.isEmpty(display)) {
723                return RecipientEntry.constructGeneratedEntry(display, token);
724            } else {
725                display = tokens[0].getAddress();
726                if (!TextUtils.isEmpty(display)) {
727                    return RecipientEntry.constructFakeEntry(display);
728                }
729            }
730        }
731        // Unable to validate the token or to create a valid token from it.
732        // Just create a chip the user can edit.
733        if (mValidator != null && !mValidator.isValid(token)) {
734            // Try fixing up the entry using the validator.
735            token = mValidator.fixText(token).toString();
736            if (!TextUtils.isEmpty(token)) {
737                // protect against the case of a validator with a null domain,
738                // which doesn't add a domain to the token
739                Rfc822Token[] tokenized = Rfc822Tokenizer.tokenize(token);
740                if (tokenized.length > 0) {
741                    token = tokenized[0].getAddress();
742                }
743            }
744        }
745        // Otherwise, fallback to just creating an editable email address chip.
746        return RecipientEntry.constructFakeEntry(token);
747    }
748
749    private boolean isValid(String text) {
750        return mValidator == null ? true : mValidator.isValid(text);
751    }
752
753    private String tokenizeAddress(String destination) {
754        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(destination);
755        if (tokens != null && tokens.length > 0) {
756            return tokens[0].getAddress();
757        }
758        return destination;
759    }
760
761    @Override
762    public void setTokenizer(Tokenizer tokenizer) {
763        mTokenizer = tokenizer;
764        super.setTokenizer(mTokenizer);
765    }
766
767    @Override
768    public void setValidator(Validator validator) {
769        mValidator = validator;
770        super.setValidator(validator);
771    }
772
773    /**
774     * We cannot use the default mechanism for replaceText. Instead,
775     * we override onItemClickListener so we can get all the associated
776     * contact information including display text, address, and id.
777     */
778    @Override
779    protected void replaceText(CharSequence text) {
780        return;
781    }
782
783    /**
784     * Dismiss any selected chips when the back key is pressed.
785     */
786    @Override
787    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
788        if (keyCode == KeyEvent.KEYCODE_BACK) {
789            clearSelectedChip();
790        }
791        return super.onKeyPreIme(keyCode, event);
792    }
793
794    /**
795     * Monitor key presses in this view to see if the user types
796     * any commit keys, which consist of ENTER, TAB, or DPAD_CENTER.
797     * If the user has entered text that has contact matches and types
798     * a commit key, create a chip from the topmost matching contact.
799     * If the user has entered text that has no contact matches and types
800     * a commit key, then create a chip from the text they have entered.
801     */
802    @Override
803    public boolean onKeyUp(int keyCode, KeyEvent event) {
804        switch (keyCode) {
805            case KeyEvent.KEYCODE_ENTER:
806            case KeyEvent.KEYCODE_DPAD_CENTER:
807                if (event.hasNoModifiers()) {
808                    if (commitDefault()) {
809                        return true;
810                    }
811                    if (mSelectedChip != null) {
812                        clearSelectedChip();
813                        return true;
814                    } else if (focusNext()) {
815                        return true;
816                    }
817                }
818                break;
819            case KeyEvent.KEYCODE_TAB:
820                if (event.hasNoModifiers()) {
821                    if (mSelectedChip != null) {
822                        clearSelectedChip();
823                    } else {
824                        commitDefault();
825                    }
826                    if (focusNext()) {
827                        return true;
828                    }
829                }
830        }
831        return super.onKeyUp(keyCode, event);
832    }
833
834    private boolean focusNext() {
835        View next = focusSearch(View.FOCUS_DOWN);
836        if (next != null) {
837            next.requestFocus();
838            return true;
839        }
840        return false;
841    }
842
843    /**
844     * Create a chip from the default selection. If the popup is showing, the
845     * default is the first item in the popup suggestions list. Otherwise, it is
846     * whatever the user had typed in. End represents where the the tokenizer
847     * should search for a token to turn into a chip.
848     * @return If a chip was created from a real contact.
849     */
850    private boolean commitDefault() {
851        Editable editable = getText();
852        int end = getSelectionEnd();
853        int start = mTokenizer.findTokenStart(editable, end);
854
855        if (shouldCreateChip(start, end)) {
856            int whatEnd = mTokenizer.findTokenEnd(getText(), start);
857            // In the middle of chip; treat this as an edit
858            // and commit the whole token.
859            if (whatEnd != getSelectionEnd()) {
860                handleEdit(start, whatEnd);
861                return true;
862            }
863            return commitChip(start, end , editable);
864        }
865        return false;
866    }
867
868    private void commitByCharacter() {
869        Editable editable = getText();
870        int end = getSelectionEnd();
871        int start = mTokenizer.findTokenStart(editable, end);
872        if (shouldCreateChip(start, end)) {
873            commitChip(start, end, editable);
874        }
875        setSelection(getText().length());
876    }
877
878    private boolean commitChip(int start, int end, Editable editable) {
879        if (getAdapter().getCount() > 0 && enoughToFilter()) {
880            // choose the first entry.
881            submitItemAtPosition(0);
882            dismissDropDown();
883            return true;
884        } else {
885            int tokenEnd = mTokenizer.findTokenEnd(editable, start);
886            String text = editable.toString().substring(start, tokenEnd).trim();
887            clearComposingText();
888            if (text != null && text.length() > 0 && !text.equals(" ")) {
889                RecipientEntry entry = createTokenizedEntry(text);
890                if (entry != null) {
891                    QwertyKeyListener.markAsReplaced(editable, start, end, "");
892                    CharSequence chipText = createChip(entry, false);
893                    editable.replace(start, end, chipText);
894                }
895                dismissDropDown();
896                return true;
897            }
898        }
899        return false;
900    }
901
902    private boolean shouldCreateChip(int start, int end) {
903        return hasFocus() && enoughToFilter() && !alreadyHasChip(start, end);
904    }
905
906    private boolean alreadyHasChip(int start, int end) {
907        RecipientChip[] chips = getSpannable().getSpans(start, end, RecipientChip.class);
908        if ((chips == null || chips.length == 0)) {
909            return false;
910        }
911        return true;
912    }
913
914    private void handleEdit(int start, int end) {
915        // This is in the middle of a chip, so select out the whole chip
916        // and commit it.
917        Editable editable = getText();
918        setSelection(end);
919        String text = getText().toString().substring(start, end);
920        RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
921        QwertyKeyListener.markAsReplaced(editable, start, end, "");
922        CharSequence chipText = createChip(entry, false);
923        editable.replace(start, getSelectionEnd(), chipText);
924        dismissDropDown();
925    }
926
927    /**
928     * If there is a selected chip, delegate the key events
929     * to the selected chip.
930     */
931    @Override
932    public boolean onKeyDown(int keyCode, KeyEvent event) {
933        if (mSelectedChip != null && keyCode == KeyEvent.KEYCODE_DEL) {
934            if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
935                mAlternatesPopup.dismiss();
936            }
937            removeChip(mSelectedChip);
938        }
939
940        if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
941            return true;
942        }
943
944        return super.onKeyDown(keyCode, event);
945    }
946
947    private Spannable getSpannable() {
948        return getText();
949    }
950
951    private int getChipStart(RecipientChip chip) {
952        return getSpannable().getSpanStart(chip);
953    }
954
955    private int getChipEnd(RecipientChip chip) {
956        return getSpannable().getSpanEnd(chip);
957    }
958
959    /**
960     * Instead of filtering on the entire contents of the edit box,
961     * this subclass method filters on the range from
962     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
963     * if the length of that range meets or exceeds {@link #getThreshold}
964     * and makes sure that the range is not already a Chip.
965     */
966    @Override
967    protected void performFiltering(CharSequence text, int keyCode) {
968        if (enoughToFilter()) {
969            int end = getSelectionEnd();
970            int start = mTokenizer.findTokenStart(text, end);
971            // If this is a RecipientChip, don't filter
972            // on its contents.
973            Spannable span = getSpannable();
974            RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
975            if (chips != null && chips.length > 0) {
976                return;
977            }
978        }
979        super.performFiltering(text, keyCode);
980    }
981
982    private void clearSelectedChip() {
983        if (mSelectedChip != null) {
984            unselectChip(mSelectedChip);
985            mSelectedChip = null;
986        }
987        setCursorVisible(true);
988    }
989
990    /**
991     * Monitor touch events in the RecipientEditTextView.
992     * If the view does not have focus, any tap on the view
993     * will just focus the view. If the view has focus, determine
994     * if the touch target is a recipient chip. If it is and the chip
995     * is not selected, select it and clear any other selected chips.
996     * If it isn't, then select that chip.
997     */
998    @Override
999    public boolean onTouchEvent(MotionEvent event) {
1000        if (!isFocused()) {
1001            // Ignore any chip taps until this view is focused.
1002            return super.onTouchEvent(event);
1003        }
1004
1005        boolean handled = super.onTouchEvent(event);
1006        int action = event.getAction();
1007        boolean chipWasSelected = false;
1008        if (action == MotionEvent.ACTION_DOWN && mSelectedChip == null) {
1009            mGestureDetector.onTouchEvent(event);
1010        }
1011        if (mCopyAddress == null && action == MotionEvent.ACTION_UP) {
1012            float x = event.getX();
1013            float y = event.getY();
1014            int offset = putOffsetInRange(getOffsetForPosition(x, y));
1015            RecipientChip currentChip = findChip(offset);
1016            if (currentChip != null) {
1017                if (action == MotionEvent.ACTION_UP) {
1018                    if (mSelectedChip != null && mSelectedChip != currentChip) {
1019                        clearSelectedChip();
1020                        mSelectedChip = selectChip(currentChip);
1021                    } else if (mSelectedChip == null) {
1022                        // Selection may have moved due to the tap event,
1023                        // but make sure we correctly reset selection to the
1024                        // end so that any unfinished chips are committed.
1025                        setSelection(getText().length());
1026                        commitDefault();
1027                        mSelectedChip = selectChip(currentChip);
1028                    } else {
1029                        onClick(mSelectedChip, offset, x, y);
1030                    }
1031                }
1032                chipWasSelected = true;
1033                handled = true;
1034            }
1035        }
1036        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
1037            clearSelectedChip();
1038        }
1039        return handled;
1040    }
1041
1042    private void scrollLineIntoView(int line) {
1043        if (mScrollView != null) {
1044            mScrollView.scrollBy(0, calculateOffsetFromBottom(line));
1045        }
1046    }
1047
1048    private void showAlternates(RecipientChip currentChip, ListPopupWindow alternatesPopup,
1049            int width, Context context) {
1050        int line = getLayout().getLineForOffset(getChipStart(currentChip));
1051        int bottom = calculateOffsetFromBottom(line);
1052        // Align the alternates popup with the left side of the View,
1053        // regardless of the position of the chip tapped.
1054        alternatesPopup.setWidth(width);
1055        alternatesPopup.setAnchorView(this);
1056        alternatesPopup.setVerticalOffset(bottom);
1057        alternatesPopup.setAdapter(createAlternatesAdapter(currentChip));
1058        alternatesPopup.setOnItemClickListener(mAlternatesListener);
1059        // Clear the checked item.
1060        mCheckedItem = -1;
1061        alternatesPopup.show();
1062        ListView listView = alternatesPopup.getListView();
1063        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
1064        // Checked item would be -1 if the adapter has not
1065        // loaded the view that should be checked yet. The
1066        // variable will be set correctly when onCheckedItemChanged
1067        // is called in a separate thread.
1068        if (mCheckedItem != -1) {
1069            listView.setItemChecked(mCheckedItem, true);
1070            mCheckedItem = -1;
1071        }
1072    }
1073
1074    private ListAdapter createAlternatesAdapter(RecipientChip chip) {
1075        return new RecipientAlternatesAdapter(getContext(), chip.getContactId(), chip.getDataId(),
1076                mAlternatesLayout, this);
1077    }
1078
1079    private ListAdapter createSingleAddressAdapter(RecipientChip currentChip) {
1080        return new SingleRecipientArrayAdapter(getContext(), mAlternatesLayout, currentChip
1081                .getEntry());
1082    }
1083
1084    @Override
1085    public void onCheckedItemChanged(int position) {
1086        ListView listView = mAlternatesPopup.getListView();
1087        if (listView != null && listView.getCheckedItemCount() == 0) {
1088            listView.setItemChecked(position, true);
1089        }
1090        mCheckedItem = position;
1091    }
1092
1093    // TODO: This algorithm will need a lot of tweaking after more people have used
1094    // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
1095    // what comes before the finger.
1096    private int putOffsetInRange(int o) {
1097        int offset = o;
1098        Editable text = getText();
1099        int length = text.length();
1100        // Remove whitespace from end to find "real end"
1101        int realLength = length;
1102        for (int i = length - 1; i >= 0; i--) {
1103            if (text.charAt(i) == ' ') {
1104                realLength--;
1105            } else {
1106                break;
1107            }
1108        }
1109
1110        // If the offset is beyond or at the end of the text,
1111        // leave it alone.
1112        if (offset >= realLength) {
1113            return offset;
1114        }
1115        Editable editable = getText();
1116        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
1117            // Keep walking backward!
1118            offset--;
1119        }
1120        return offset;
1121    }
1122
1123    private int findText(Editable text, int offset) {
1124        if (text.charAt(offset) != ' ') {
1125            return offset;
1126        }
1127        return -1;
1128    }
1129
1130    private RecipientChip findChip(int offset) {
1131        RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class);
1132        // Find the chip that contains this offset.
1133        for (int i = 0; i < chips.length; i++) {
1134            RecipientChip chip = chips[i];
1135            int start = getChipStart(chip);
1136            int end = getChipEnd(chip);
1137            if (offset >= start && offset <= end) {
1138                return chip;
1139            }
1140        }
1141        return null;
1142    }
1143
1144    private CharSequence createChip(RecipientEntry entry, boolean pressed) {
1145        String displayText = entry.getDestination();
1146        displayText = (String) mTokenizer.terminateToken(displayText);
1147        // Always leave a blank space at the end of a chip.
1148        int textLength = displayText.length()-1;
1149        SpannableString chipText = new SpannableString(displayText);
1150        int end = getSelectionEnd();
1151        int start = mTokenizer.findTokenStart(getText(), end);
1152        try {
1153            RecipientChip chip = constructChipSpan(entry, start, pressed);
1154            chipText.setSpan(chip, 0, textLength,
1155                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1156            chip.setOriginalText(chipText.toString());
1157        } catch (NullPointerException e) {
1158            Log.e(TAG, e.getMessage(), e);
1159            return null;
1160        }
1161
1162        return chipText;
1163    }
1164
1165    /**
1166     * When an item in the suggestions list has been clicked, create a chip from the
1167     * contact information of the selected item.
1168     */
1169    @Override
1170    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1171        submitItemAtPosition(position);
1172    }
1173
1174    private void submitItemAtPosition(int position) {
1175        RecipientEntry entry = createValidatedEntry(
1176                (RecipientEntry)getAdapter().getItem(position));
1177        if (entry == null) {
1178            return;
1179        }
1180        clearComposingText();
1181
1182        int end = getSelectionEnd();
1183        int start = mTokenizer.findTokenStart(getText(), end);
1184
1185        Editable editable = getText();
1186        QwertyKeyListener.markAsReplaced(editable, start, end, "");
1187        CharSequence chip = createChip(entry, false);
1188        if (chip != null) {
1189            editable.replace(start, end, chip);
1190        }
1191    }
1192
1193    private RecipientEntry createValidatedEntry(RecipientEntry item) {
1194        if (item == null) {
1195            return null;
1196        }
1197        final RecipientEntry entry;
1198        // If the display name and the address are the same, or if this is a
1199        // valid contact, but the destination is invalid, then make this a fake
1200        // recipient that is editable.
1201        String destination = item.getDestination();
1202        if (TextUtils.isEmpty(item.getDisplayName())
1203                || TextUtils.equals(item.getDisplayName(), destination)
1204                || (mValidator != null && !mValidator.isValid(destination))) {
1205            entry = RecipientEntry.constructFakeEntry(destination);
1206        } else {
1207            entry = item;
1208        }
1209        return entry;
1210    }
1211
1212    /** Returns a collection of contact Id for each chip inside this View. */
1213    /* package */ Collection<Long> getContactIds() {
1214        final Set<Long> result = new HashSet<Long>();
1215        RecipientChip[] chips = getRecipients();
1216        if (chips != null) {
1217            for (RecipientChip chip : chips) {
1218                result.add(chip.getContactId());
1219            }
1220        }
1221        return result;
1222    }
1223
1224    private RecipientChip[] getRecipients() {
1225        return getSpannable().getSpans(0, getText().length(), RecipientChip.class);
1226    }
1227
1228    private RecipientChip[] getSortedRecipients() {
1229        ArrayList<RecipientChip> recipientsList = new ArrayList<RecipientChip>(Arrays
1230                .asList(getRecipients()));
1231        final Spannable spannable = getSpannable();
1232        Collections.sort(recipientsList, new Comparator<RecipientChip>() {
1233
1234            @Override
1235            public int compare(RecipientChip first, RecipientChip second) {
1236                int firstStart = spannable.getSpanStart(first);
1237                int secondStart = spannable.getSpanStart(second);
1238                if (firstStart < secondStart) {
1239                    return -1;
1240                } else if (firstStart > secondStart) {
1241                    return 1;
1242                } else {
1243                    return 0;
1244                }
1245            }
1246        });
1247        return recipientsList.toArray(new RecipientChip[recipientsList.size()]);
1248    }
1249
1250    /** Returns a collection of data Id for each chip inside this View. May be null. */
1251    /* package */ Collection<Long> getDataIds() {
1252        final Set<Long> result = new HashSet<Long>();
1253        RecipientChip [] chips = getRecipients();
1254        if (chips != null) {
1255            for (RecipientChip chip : chips) {
1256                result.add(chip.getDataId());
1257            }
1258        }
1259        return result;
1260    }
1261
1262
1263    @Override
1264    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
1265        return false;
1266    }
1267
1268    @Override
1269    public void onDestroyActionMode(ActionMode mode) {
1270    }
1271
1272    @Override
1273    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
1274        return false;
1275    }
1276
1277    /**
1278     * No chips are selectable.
1279     */
1280    @Override
1281    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
1282        return false;
1283    }
1284
1285    /**
1286     * Create the more chip. The more chip is text that replaces any chips that
1287     * do not fit in the pre-defined available space when the
1288     * RecipientEditTextView loses focus.
1289     */
1290    private void createMoreChip() {
1291        if (!mShouldShrink) {
1292            return;
1293        }
1294
1295        ImageSpan[] tempMore = getSpannable().getSpans(0, getText().length(), MoreImageSpan.class);
1296        if (tempMore.length > 0) {
1297            getSpannable().removeSpan(tempMore[0]);
1298        }
1299        RecipientChip[] recipients = getSortedRecipients();
1300        if (recipients == null || recipients.length <= CHIP_LIMIT) {
1301            mMoreChip = null;
1302            return;
1303        }
1304        Spannable spannable = getSpannable();
1305        int numRecipients = recipients.length;
1306        int overage = numRecipients - CHIP_LIMIT;
1307        String moreText = String.format(mMoreItem.getText().toString(), overage);
1308        TextPaint morePaint = new TextPaint(getPaint());
1309        morePaint.setTextSize(mMoreItem.getTextSize());
1310        morePaint.setColor(mMoreItem.getCurrentTextColor());
1311        int width = (int)morePaint.measureText(moreText) + mMoreItem.getPaddingLeft()
1312                + mMoreItem.getPaddingRight();
1313        int height = getLineHeight();
1314        Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1315        Canvas canvas = new Canvas(drawable);
1316        canvas.drawText(moreText, 0, moreText.length(), 0, height - getLayout().getLineDescent(0),
1317                morePaint);
1318
1319        Drawable result = new BitmapDrawable(getResources(), drawable);
1320        result.setBounds(0, 0, width, height);
1321        MoreImageSpan moreSpan = new MoreImageSpan(result);
1322        // Remove the overage chips.
1323        if (recipients == null || recipients.length == 0) {
1324            Log.w(TAG,
1325                    "We have recipients. Tt should not be possible to have zero RecipientChips.");
1326            mMoreChip = null;
1327            return;
1328        }
1329        mRemovedSpans = new ArrayList<RecipientChip>();
1330        int totalReplaceStart = 0;
1331        int totalReplaceEnd = 0;
1332        Editable text = getText();
1333        for (int i = numRecipients - overage; i < recipients.length; i++) {
1334            mRemovedSpans.add(recipients[i]);
1335            if (i == numRecipients - overage) {
1336                totalReplaceStart = spannable.getSpanStart(recipients[i]);
1337            }
1338            if (i == recipients.length - 1) {
1339                totalReplaceEnd = spannable.getSpanEnd(recipients[i]);
1340            }
1341            if (mTemporaryRecipients == null || !mTemporaryRecipients.contains(recipients[i])) {
1342                int spanStart = spannable.getSpanStart(recipients[i]);
1343                int spanEnd = spannable.getSpanEnd(recipients[i]);
1344                recipients[i].setOriginalText(text.toString().substring(spanStart, spanEnd));
1345            }
1346            spannable.removeSpan(recipients[i]);
1347        }
1348        int end = Math.max(totalReplaceStart, totalReplaceEnd);
1349        int start = Math.min(totalReplaceStart, totalReplaceEnd);
1350        SpannableString chipText = new SpannableString(text.subSequence(start, end));
1351        chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1352        text.replace(start, end, chipText);
1353        mMoreChip = moreSpan;
1354    }
1355
1356    /**
1357     * Replace the more chip, if it exists, with all of the recipient chips it had
1358     * replaced when the RecipientEditTextView gains focus.
1359     */
1360    private void removeMoreChip() {
1361        if (mMoreChip != null) {
1362            Spannable span = getSpannable();
1363            span.removeSpan(mMoreChip);
1364            mMoreChip = null;
1365            // Re-add the spans that were removed.
1366            if (mRemovedSpans != null && mRemovedSpans.size() > 0) {
1367                // Recreate each removed span.
1368                Editable editable = getText();
1369                for (RecipientChip chip : mRemovedSpans) {
1370                    int chipStart;
1371                    int chipEnd;
1372                    String token;
1373                    // Need to find the location of the chip, again.
1374                    token = (String) chip.getOriginalText();
1375                    chipStart = editable.toString().indexOf(token);
1376                    // -1 for the space!
1377                    chipEnd = Math.min(editable.length(), chipStart + token.length());
1378                    // Only set the span if we found a matching token.
1379                    if (chipStart != -1) {
1380                        editable.setSpan(chip, chipStart, chipEnd,
1381                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1382                    }
1383                }
1384                mRemovedSpans.clear();
1385            }
1386        }
1387    }
1388
1389    /**
1390     * Show specified chip as selected. If the RecipientChip is just an email address,
1391     * selecting the chip will take the contents of the chip and place it at
1392     * the end of the RecipientEditTextView for inline editing. If the
1393     * RecipientChip is a complete contact, then selecting the chip
1394     * will change the background color of the chip, show the delete icon,
1395     * and a popup window with the address in use highlighted and any other
1396     * alternate addresses for the contact.
1397     * @param currentChip Chip to select.
1398     * @return A RecipientChip in the selected state or null if the chip
1399     * just contained an email address.
1400     */
1401    public RecipientChip selectChip(RecipientChip currentChip) {
1402        if (currentChip.getContactId() == RecipientEntry.INVALID_CONTACT) {
1403            CharSequence text = currentChip.getValue();
1404            Editable editable = getText();
1405            removeChip(currentChip);
1406            editable.append(text);
1407            setCursorVisible(true);
1408            setSelection(editable.length());
1409            return new RecipientChip(null, RecipientEntry.constructFakeEntry((String) text), -1);
1410        } else if (currentChip.getContactId() == RecipientEntry.GENERATED_CONTACT) {
1411            int start = getChipStart(currentChip);
1412            int end = getChipEnd(currentChip);
1413            getSpannable().removeSpan(currentChip);
1414            RecipientChip newChip;
1415            try {
1416                newChip = constructChipSpan(currentChip.getEntry(), start, true);
1417            } catch (NullPointerException e) {
1418                Log.e(TAG, e.getMessage(), e);
1419                return null;
1420            }
1421            Editable editable = getText();
1422            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1423            if (start == -1 || end == -1) {
1424                Log.d(TAG, "The chip being selected no longer exists but should.");
1425            } else {
1426                editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1427            }
1428            newChip.setSelected(true);
1429            if (newChip.getEntry().getContactId() == RecipientEntry.INVALID_CONTACT) {
1430                scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
1431            }
1432            showAddress(newChip, mAddressPopup, getWidth(), getContext());
1433            setCursorVisible(false);
1434            return newChip;
1435        } else {
1436            int start = getChipStart(currentChip);
1437            int end = getChipEnd(currentChip);
1438            getSpannable().removeSpan(currentChip);
1439            RecipientChip newChip;
1440            try {
1441                newChip = constructChipSpan(currentChip.getEntry(), start, true);
1442            } catch (NullPointerException e) {
1443                Log.e(TAG, e.getMessage(), e);
1444                return null;
1445            }
1446            Editable editable = getText();
1447            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1448            if (start == -1 || end == -1) {
1449                Log.d(TAG, "The chip being selected no longer exists but should.");
1450            } else {
1451                editable.setSpan(newChip, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1452            }
1453            newChip.setSelected(true);
1454            if (newChip.getEntry().getContactId() == RecipientEntry.INVALID_CONTACT) {
1455                scrollLineIntoView(getLayout().getLineForOffset(getChipStart(newChip)));
1456            }
1457            showAlternates(newChip, mAlternatesPopup, getWidth(), getContext());
1458            setCursorVisible(false);
1459            return newChip;
1460        }
1461    }
1462
1463
1464    private void showAddress(final RecipientChip currentChip, final ListPopupWindow popup,
1465            int width, Context context) {
1466        int line = getLayout().getLineForOffset(getChipStart(currentChip));
1467        int bottom = calculateOffsetFromBottom(line);
1468        // Align the alternates popup with the left side of the View,
1469        // regardless of the position of the chip tapped.
1470        popup.setWidth(width);
1471        popup.setAnchorView(this);
1472        popup.setVerticalOffset(bottom);
1473        popup.setAdapter(createSingleAddressAdapter(currentChip));
1474        popup.setOnItemClickListener(new OnItemClickListener() {
1475            @Override
1476            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1477                unselectChip(currentChip);
1478                popup.dismiss();
1479            }
1480        });
1481        popup.show();
1482        ListView listView = popup.getListView();
1483        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
1484        listView.setItemChecked(0, true);
1485    }
1486
1487    /**
1488     * Remove selection from this chip. Unselecting a RecipientChip will render
1489     * the chip without a delete icon and with an unfocused background. This
1490     * is called when the RecipientChip no longer has focus.
1491     */
1492    public void unselectChip(RecipientChip chip) {
1493        int start = getChipStart(chip);
1494        int end = getChipEnd(chip);
1495        Editable editable = getText();
1496        mSelectedChip = null;
1497        if (start == -1 || end == -1) {
1498            Log.e(TAG, "The chip being unselected no longer exists.");
1499        } else {
1500            getSpannable().removeSpan(chip);
1501            QwertyKeyListener.markAsReplaced(editable, start, end, "");
1502            editable.removeSpan(chip);
1503            try {
1504                editable.setSpan(constructChipSpan(chip.getEntry(), start, false), start, end,
1505                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1506            } catch (NullPointerException e) {
1507                Log.e(TAG, e.getMessage(), e);
1508            }
1509        }
1510        setCursorVisible(true);
1511        setSelection(editable.length());
1512        if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
1513            mAlternatesPopup.dismiss();
1514        }
1515    }
1516
1517
1518    /**
1519     * Return whether this chip contains the position passed in.
1520     */
1521    public boolean matchesChip(RecipientChip chip, int offset) {
1522        int start = getChipStart(chip);
1523        int end = getChipEnd(chip);
1524        if (start == -1 || end == -1) {
1525            return false;
1526        }
1527        return (offset >= start && offset <= end);
1528    }
1529
1530
1531    /**
1532     * Return whether a touch event was inside the delete target of
1533     * a selected chip. It is in the delete target if:
1534     * 1) the x and y points of the event are within the
1535     * delete assset.
1536     * 2) the point tapped would have caused a cursor to appear
1537     * right after the selected chip.
1538     * @return boolean
1539     */
1540    private boolean isInDelete(RecipientChip chip, int offset, float x, float y) {
1541        // Figure out the bounds of this chip and whether or not
1542        // the user clicked in the X portion.
1543        return chip.isSelected() && offset == getChipEnd(chip);
1544    }
1545
1546    /**
1547     * Remove the chip and any text associated with it from the RecipientEditTextView.
1548     */
1549    private void removeChip(RecipientChip chip) {
1550        Spannable spannable = getSpannable();
1551        int spanStart = spannable.getSpanStart(chip);
1552        int spanEnd = spannable.getSpanEnd(chip);
1553        Editable text = getText();
1554        int toDelete = spanEnd;
1555        boolean wasSelected = chip == mSelectedChip;
1556        // Clear that there is a selected chip before updating any text.
1557        if (wasSelected) {
1558            mSelectedChip = null;
1559        }
1560        // Always remove trailing spaces when removing a chip.
1561        while (toDelete >= 0 && toDelete < text.length() && text.charAt(toDelete) == ' ') {
1562            toDelete++;
1563        }
1564        spannable.removeSpan(chip);
1565        text.delete(spanStart, toDelete);
1566        if (wasSelected) {
1567            clearSelectedChip();
1568        }
1569    }
1570
1571    /**
1572     * Replace this currently selected chip with a new chip
1573     * that uses the contact data provided.
1574     */
1575    public void replaceChip(RecipientChip chip, RecipientEntry entry) {
1576        boolean wasSelected = chip == mSelectedChip;
1577        if (wasSelected) {
1578            mSelectedChip = null;
1579        }
1580        int start = getChipStart(chip);
1581        int end = getChipEnd(chip);
1582        getSpannable().removeSpan(chip);
1583        Editable editable = getText();
1584        CharSequence chipText = createChip(entry, false);
1585        if (start == -1 || end == -1) {
1586            Log.e(TAG, "The chip to replace does not exist but should.");
1587            editable.insert(0, chipText);
1588        } else {
1589            // There may be a space to replace with this chip's new associated
1590            // space. Check for it.
1591            int toReplace = end;
1592            while (toReplace >= 0 && toReplace < editable.length()
1593                    && editable.charAt(toReplace) == ' ') {
1594                toReplace++;
1595            }
1596            editable.replace(start, toReplace, chipText);
1597        }
1598        setCursorVisible(true);
1599        if (wasSelected) {
1600            clearSelectedChip();
1601        }
1602    }
1603
1604    /**
1605     * Handle click events for a chip. When a selected chip receives a click
1606     * event, see if that event was in the delete icon. If so, delete it.
1607     * Otherwise, unselect the chip.
1608     */
1609    public void onClick(RecipientChip chip, int offset, float x, float y) {
1610        if (chip.isSelected()) {
1611            if (isInDelete(chip, offset, x, y)) {
1612                removeChip(chip);
1613            } else {
1614                clearSelectedChip();
1615            }
1616        }
1617    }
1618
1619    private boolean chipsPending() {
1620        return mPendingChipsCount > 0 || (mRemovedSpans != null && mRemovedSpans.size() > 0);
1621    }
1622
1623    @Override
1624    public void removeTextChangedListener(TextWatcher watcher) {
1625        mTextWatcher = null;
1626        super.removeTextChangedListener(watcher);
1627    }
1628
1629    private class RecipientTextWatcher implements TextWatcher {
1630        @Override
1631        public void afterTextChanged(Editable s) {
1632            // If the text has been set to null or empty, make sure we remove
1633            // all the spans we applied.
1634            if (TextUtils.isEmpty(s)) {
1635                // Remove all the chips spans.
1636                Spannable spannable = getSpannable();
1637                RecipientChip[] chips = spannable.getSpans(0, getText().length(),
1638                        RecipientChip.class);
1639                for (RecipientChip chip : chips) {
1640                    spannable.removeSpan(chip);
1641                }
1642                if (mMoreChip != null) {
1643                    spannable.removeSpan(mMoreChip);
1644                }
1645                return;
1646            }
1647            // Get whether there are any recipients pending addition to the
1648            // view. If there are, don't do anything in the text watcher.
1649            if (chipsPending()) {
1650                return;
1651            }
1652            if (mSelectedChip != null) {
1653                setCursorVisible(true);
1654                setSelection(getText().length());
1655                clearSelectedChip();
1656            }
1657            int length = s.length();
1658            // Make sure there is content there to parse and that it is
1659            // not just the commit character.
1660            if (length > 1) {
1661                char last;
1662                int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
1663                int len = length() - 1;
1664                if (end != len) {
1665                    last = s.charAt(end);
1666                } else {
1667                    last = s.charAt(len);
1668                }
1669                if (last == COMMIT_CHAR_SEMICOLON || last == COMMIT_CHAR_COMMA) {
1670                    commitByCharacter();
1671                } else if (last == COMMIT_CHAR_SPACE) {
1672                    // Check if this is a valid email address. If it is,
1673                    // commit it.
1674                    String text = getText().toString();
1675                    int tokenStart = mTokenizer.findTokenStart(text, getSelectionEnd());
1676                    String sub = text.substring(tokenStart, mTokenizer.findTokenEnd(text,
1677                            tokenStart));
1678                    if (mValidator != null && mValidator.isValid(sub)) {
1679                        commitByCharacter();
1680                    }
1681                }
1682            }
1683        }
1684
1685        @Override
1686        public void onTextChanged(CharSequence s, int start, int before, int count) {
1687            // Do nothing.
1688        }
1689
1690        @Override
1691        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1692        }
1693    }
1694
1695    private class RecipientReplacementTask extends AsyncTask<Void, Void, Void> {
1696        private RecipientChip createFreeChip(RecipientEntry entry) {
1697            String displayText = entry.getDestination();
1698            if (displayText.indexOf(",") == -1) {
1699                displayText = (String) mTokenizer.terminateToken(displayText);
1700            }
1701            try {
1702                return constructChipSpan(entry, -1, false);
1703            } catch (NullPointerException e) {
1704                Log.e(TAG, e.getMessage(), e);
1705                return null;
1706            }
1707        }
1708
1709        @Override
1710        protected Void doInBackground(Void... params) {
1711            if (mIndividualReplacements != null) {
1712                mIndividualReplacements.cancel(true);
1713            }
1714            // For each chip in the list, look up the matching contact.
1715            // If there is a match, replace that chip with the matching
1716            // chip.
1717            final ArrayList<RecipientChip> originalRecipients = new ArrayList<RecipientChip>();
1718            RecipientChip[] existingChips = getSortedRecipients();
1719            for (int i = 0; i < existingChips.length; i++) {
1720                originalRecipients.add(existingChips[i]);
1721            }
1722            if (mRemovedSpans != null) {
1723                originalRecipients.addAll(mRemovedSpans);
1724            }
1725            String[] addresses = new String[originalRecipients.size()];
1726            for (int i = 0; i < originalRecipients.size(); i++) {
1727                addresses[i] = originalRecipients.get(i).getEntry().getDestination();
1728            }
1729            HashMap<String, RecipientEntry> entries = RecipientAlternatesAdapter
1730                    .getMatchingRecipients(getContext(), addresses);
1731            final ArrayList<RecipientChip> replacements = new ArrayList<RecipientChip>();
1732            for (final RecipientChip temp : originalRecipients) {
1733                RecipientEntry entry = null;
1734                if (RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())
1735                        && getSpannable().getSpanStart(temp) != -1) {
1736                    // Replace this.
1737                    entry = createValidatedEntry(entries.get(tokenizeAddress(temp.getEntry()
1738                            .getDestination())));
1739                }
1740                if (entry != null) {
1741                    replacements.add(createFreeChip(entry));
1742                } else {
1743                    replacements.add(temp);
1744                }
1745            }
1746            if (replacements != null && replacements.size() > 0) {
1747                mHandler.post(new Runnable() {
1748                    @Override
1749                    public void run() {
1750                        SpannableStringBuilder text = new SpannableStringBuilder(getText()
1751                                .toString());
1752                        Editable oldText = getText();
1753                        int start, end;
1754                        int i = 0;
1755                        for (RecipientChip chip : originalRecipients) {
1756                            start = oldText.getSpanStart(chip);
1757                            if (start != -1) {
1758                                end = oldText.getSpanEnd(chip);
1759                                text.removeSpan(chip);
1760                                // Leave a spot for the space!
1761                                RecipientChip replacement = replacements.get(i);
1762                                text.setSpan(replacement, start, end,
1763                                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1764                                replacement.setOriginalText(text.toString().substring(start, end));
1765                            }
1766                            i++;
1767                        }
1768                        Editable editable = getText();
1769                        editable.clear();
1770                        editable.insert(0, text);
1771                        originalRecipients.clear();
1772                    }
1773                });
1774            }
1775            return null;
1776        }
1777    }
1778
1779    private class IndividualReplacementTask extends AsyncTask<Object, Void, Void> {
1780        @SuppressWarnings("unchecked")
1781        @Override
1782        protected Void doInBackground(Object... params) {
1783            // For each chip in the list, look up the matching contact.
1784            // If there is a match, replace that chip with the matching
1785            // chip.
1786            final ArrayList<RecipientChip> originalRecipients =
1787                (ArrayList<RecipientChip>) params[0];
1788            String[] addresses = new String[originalRecipients.size()];
1789            for (int i = 0; i < originalRecipients.size(); i++) {
1790                addresses[i] = originalRecipients.get(i).getEntry().getDestination();
1791            }
1792            HashMap<String, RecipientEntry> entries = RecipientAlternatesAdapter
1793                    .getMatchingRecipients(getContext(), addresses);
1794            for (final RecipientChip temp : originalRecipients) {
1795                if (RecipientEntry.isCreatedRecipient(temp.getEntry().getContactId())
1796                        && getSpannable().getSpanStart(temp) != -1) {
1797                    // Replace this.
1798                    final RecipientEntry entry = createValidatedEntry(entries
1799                            .get(tokenizeAddress(temp.getEntry().getDestination())));
1800                    if (entry != null) {
1801                        mHandler.post(new Runnable() {
1802                            @Override
1803                            public void run() {
1804                                replaceChip(temp, entry);
1805                            }
1806                        });
1807                    }
1808                }
1809            }
1810            return null;
1811        }
1812    }
1813
1814
1815    /**
1816     * MoreImageSpan is a simple class created for tracking the existence of a
1817     * more chip across activity restarts/
1818     */
1819    private class MoreImageSpan extends ImageSpan {
1820        public MoreImageSpan(Drawable b) {
1821            super(b);
1822        }
1823    }
1824
1825    @Override
1826    public boolean onDown(MotionEvent e) {
1827        return false;
1828    }
1829
1830    @Override
1831    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
1832        // Do nothing.
1833        return false;
1834    }
1835
1836    @Override
1837    public void onLongPress(MotionEvent event) {
1838        if (mSelectedChip != null) {
1839            return;
1840        }
1841        float x = event.getX();
1842        float y = event.getY();
1843        int offset = putOffsetInRange(getOffsetForPosition(x, y));
1844        RecipientChip currentChip = findChip(offset);
1845        if (currentChip != null) {
1846            // Copy the selected chip email address.
1847            showCopyDialog(currentChip.getEntry().getDestination());
1848        }
1849    }
1850
1851    private void showCopyDialog(final String address) {
1852        mCopyAddress = address;
1853        mCopyDialog.setTitle(address);
1854        mCopyDialog.setContentView(mCopyViewRes);
1855        mCopyDialog.setCancelable(true);
1856        mCopyDialog.setCanceledOnTouchOutside(true);
1857        mCopyDialog.findViewById(android.R.id.button1).setOnClickListener(this);
1858        mCopyDialog.setOnDismissListener(this);
1859        mCopyDialog.show();
1860    }
1861
1862    @Override
1863    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
1864        // Do nothing.
1865        return false;
1866    }
1867
1868    @Override
1869    public void onShowPress(MotionEvent e) {
1870        // Do nothing.
1871    }
1872
1873    @Override
1874    public boolean onSingleTapUp(MotionEvent e) {
1875        // Do nothing.
1876        return false;
1877    }
1878
1879    @Override
1880    public void onDismiss(DialogInterface dialog) {
1881        mCopyAddress = null;
1882    }
1883
1884    @Override
1885    public void onClick(View v) {
1886        // Copy this to the clipboard.
1887        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(
1888                Context.CLIPBOARD_SERVICE);
1889        clipboard.setPrimaryClip(ClipData.newPlainText("", mCopyAddress));
1890        mCopyDialog.dismiss();
1891    }
1892}
1893