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