Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.database.DataSetObserver;
     22 import android.graphics.Rect;
     23 import android.graphics.drawable.Drawable;
     24 import android.text.Editable;
     25 import android.text.Selection;
     26 import android.text.TextUtils;
     27 import android.text.TextWatcher;
     28 import android.util.AttributeSet;
     29 import android.util.Log;
     30 import android.view.KeyEvent;
     31 import android.view.LayoutInflater;
     32 import android.view.MotionEvent;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 import android.view.WindowManager;
     36 import android.view.inputmethod.CompletionInfo;
     37 import android.view.inputmethod.InputMethodManager;
     38 import android.view.inputmethod.EditorInfo;
     39 
     40 import com.android.internal.R;
     41 
     42 
     43 /**
     44  * <p>An editable text view that shows completion suggestions automatically
     45  * while the user is typing. The list of suggestions is displayed in a drop
     46  * down menu from which the user can choose an item to replace the content
     47  * of the edit box with.</p>
     48  *
     49  * <p>The drop down can be dismissed at any time by pressing the back key or,
     50  * if no item is selected in the drop down, by pressing the enter/dpad center
     51  * key.</p>
     52  *
     53  * <p>The list of suggestions is obtained from a data adapter and appears
     54  * only after a given number of characters defined by
     55  * {@link #getThreshold() the threshold}.</p>
     56  *
     57  * <p>The following code snippet shows how to create a text view which suggests
     58  * various countries names while the user is typing:</p>
     59  *
     60  * <pre class="prettyprint">
     61  * public class CountriesActivity extends Activity {
     62  *     protected void onCreate(Bundle icicle) {
     63  *         super.onCreate(icicle);
     64  *         setContentView(R.layout.countries);
     65  *
     66  *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
     67  *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
     68  *         AutoCompleteTextView textView = (AutoCompleteTextView)
     69  *                 findViewById(R.id.countries_list);
     70  *         textView.setAdapter(adapter);
     71  *     }
     72  *
     73  *     private static final String[] COUNTRIES = new String[] {
     74  *         "Belgium", "France", "Italy", "Germany", "Spain"
     75  *     };
     76  * }
     77  * </pre>
     78  *
     79  * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-autocomplete.html">Auto Complete
     80  * tutorial</a>.</p>
     81  *
     82  * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
     83  * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
     84  * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView
     85  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector
     86  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
     87  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
     88  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
     89  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownVerticalOffset
     90  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHorizontalOffset
     91  */
     92 public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
     93     static final boolean DEBUG = false;
     94     static final String TAG = "AutoCompleteTextView";
     95 
     96     private static final int HINT_VIEW_ID = 0x17;
     97 
     98     /**
     99      * This value controls the length of time that the user
    100      * must leave a pointer down without scrolling to expand
    101      * the autocomplete dropdown list to cover the IME.
    102      */
    103     private static final int EXPAND_LIST_TIMEOUT = 250;
    104 
    105     private CharSequence mHintText;
    106     private int mHintResource;
    107 
    108     private ListAdapter mAdapter;
    109     private Filter mFilter;
    110     private int mThreshold;
    111 
    112     private PopupWindow mPopup;
    113     private DropDownListView mDropDownList;
    114     private int mDropDownVerticalOffset;
    115     private int mDropDownHorizontalOffset;
    116     private int mDropDownAnchorId;
    117     private View mDropDownAnchorView;  // view is retrieved lazily from id once needed
    118     private int mDropDownWidth;
    119     private int mDropDownHeight;
    120     private final Rect mTempRect = new Rect();
    121 
    122     private Drawable mDropDownListHighlight;
    123 
    124     private AdapterView.OnItemClickListener mItemClickListener;
    125     private AdapterView.OnItemSelectedListener mItemSelectedListener;
    126 
    127     private final DropDownItemClickListener mDropDownItemClickListener =
    128             new DropDownItemClickListener();
    129 
    130     private boolean mDropDownAlwaysVisible = false;
    131 
    132     private boolean mDropDownDismissedOnCompletion = true;
    133 
    134     private boolean mForceIgnoreOutsideTouch = false;
    135 
    136     private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    137     private boolean mOpenBefore;
    138 
    139     private Validator mValidator = null;
    140 
    141     private boolean mBlockCompletion;
    142 
    143     private ListSelectorHider mHideSelector;
    144     private Runnable mShowDropDownRunnable;
    145     private Runnable mResizePopupRunnable = new ResizePopupRunnable();
    146 
    147     private PassThroughClickListener mPassThroughClickListener;
    148     private PopupDataSetObserver mObserver;
    149 
    150     public AutoCompleteTextView(Context context) {
    151         this(context, null);
    152     }
    153 
    154     public AutoCompleteTextView(Context context, AttributeSet attrs) {
    155         this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
    156     }
    157 
    158     public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
    159         super(context, attrs, defStyle);
    160 
    161         mPopup = new PopupWindow(context, attrs,
    162                 com.android.internal.R.attr.autoCompleteTextViewStyle);
    163         mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    164 
    165         TypedArray a =
    166             context.obtainStyledAttributes(
    167                 attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0);
    168 
    169         mThreshold = a.getInt(
    170                 R.styleable.AutoCompleteTextView_completionThreshold, 2);
    171 
    172         mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint);
    173 
    174         mDropDownListHighlight = a.getDrawable(
    175                 R.styleable.AutoCompleteTextView_dropDownSelector);
    176         mDropDownVerticalOffset = (int)
    177                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f);
    178         mDropDownHorizontalOffset = (int)
    179                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f);
    180 
    181         // Get the anchor's id now, but the view won't be ready, so wait to actually get the
    182         // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later.
    183         // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return
    184         // this TextView, as a default anchoring point.
    185         mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor,
    186                 View.NO_ID);
    187 
    188         // For dropdown width, the developer can specify a specific width, or MATCH_PARENT
    189         // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view).
    190         mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth,
    191                 ViewGroup.LayoutParams.WRAP_CONTENT);
    192         mDropDownHeight = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight,
    193                 ViewGroup.LayoutParams.WRAP_CONTENT);
    194 
    195         mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
    196                 R.layout.simple_dropdown_hint);
    197 
    198         // Always turn on the auto complete input type flag, since it
    199         // makes no sense to use this widget without it.
    200         int inputType = getInputType();
    201         if ((inputType&EditorInfo.TYPE_MASK_CLASS)
    202                 == EditorInfo.TYPE_CLASS_TEXT) {
    203             inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
    204             setRawInputType(inputType);
    205         }
    206 
    207         a.recycle();
    208 
    209         setFocusable(true);
    210 
    211         addTextChangedListener(new MyWatcher());
    212 
    213         mPassThroughClickListener = new PassThroughClickListener();
    214         super.setOnClickListener(mPassThroughClickListener);
    215     }
    216 
    217     @Override
    218     public void setOnClickListener(OnClickListener listener) {
    219         mPassThroughClickListener.mWrapped = listener;
    220     }
    221 
    222     /**
    223      * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
    224      */
    225     private void onClickImpl() {
    226         // If the dropdown is showing, bring the keyboard to the front
    227         // when the user touches the text field.
    228         if (mPopup.isShowing()) {
    229             ensureImeVisible(true);
    230         }
    231     }
    232 
    233     /**
    234      * <p>Sets the optional hint text that is displayed at the bottom of the
    235      * the matching list.  This can be used as a cue to the user on how to
    236      * best use the list, or to provide extra information.</p>
    237      *
    238      * @param hint the text to be displayed to the user
    239      *
    240      * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
    241      */
    242     public void setCompletionHint(CharSequence hint) {
    243         mHintText = hint;
    244     }
    245 
    246     /**
    247      * <p>Returns the current width for the auto-complete drop down list. This can
    248      * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
    249      * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
    250      *
    251      * @return the width for the drop down list
    252      *
    253      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
    254      */
    255     public int getDropDownWidth() {
    256         return mDropDownWidth;
    257     }
    258 
    259     /**
    260      * <p>Sets the current width for the auto-complete drop down list. This can
    261      * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
    262      * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
    263      *
    264      * @param width the width to use
    265      *
    266      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
    267      */
    268     public void setDropDownWidth(int width) {
    269         mDropDownWidth = width;
    270     }
    271 
    272     /**
    273      * <p>Returns the current height for the auto-complete drop down list. This can
    274      * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
    275      * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
    276      * of the drop down's content.</p>
    277      *
    278      * @return the height for the drop down list
    279      *
    280      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
    281      */
    282     public int getDropDownHeight() {
    283         return mDropDownHeight;
    284     }
    285 
    286     /**
    287      * <p>Sets the current height for the auto-complete drop down list. This can
    288      * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
    289      * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
    290      * of the drop down's content.</p>
    291      *
    292      * @param height the height to use
    293      *
    294      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
    295      */
    296     public void setDropDownHeight(int height) {
    297         mDropDownHeight = height;
    298     }
    299 
    300     /**
    301      * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
    302      *
    303      * @return the view's id, or {@link View#NO_ID} if none specified
    304      *
    305      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
    306      */
    307     public int getDropDownAnchor() {
    308         return mDropDownAnchorId;
    309     }
    310 
    311     /**
    312      * <p>Sets the view to which the auto-complete drop down list should anchor. The view
    313      * corresponding to this id will not be loaded until the next time it is needed to avoid
    314      * loading a view which is not yet instantiated.</p>
    315      *
    316      * @param id the id to anchor the drop down list view to
    317      *
    318      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
    319      */
    320     public void setDropDownAnchor(int id) {
    321         mDropDownAnchorId = id;
    322         mDropDownAnchorView = null;
    323     }
    324 
    325     /**
    326      * <p>Gets the background of the auto-complete drop-down list.</p>
    327      *
    328      * @return the background drawable
    329      *
    330      * @attr ref android.R.styleable#PopupWindow_popupBackground
    331      */
    332     public Drawable getDropDownBackground() {
    333         return mPopup.getBackground();
    334     }
    335 
    336     /**
    337      * <p>Sets the background of the auto-complete drop-down list.</p>
    338      *
    339      * @param d the drawable to set as the background
    340      *
    341      * @attr ref android.R.styleable#PopupWindow_popupBackground
    342      */
    343     public void setDropDownBackgroundDrawable(Drawable d) {
    344         mPopup.setBackgroundDrawable(d);
    345     }
    346 
    347     /**
    348      * <p>Sets the background of the auto-complete drop-down list.</p>
    349      *
    350      * @param id the id of the drawable to set as the background
    351      *
    352      * @attr ref android.R.styleable#PopupWindow_popupBackground
    353      */
    354     public void setDropDownBackgroundResource(int id) {
    355         mPopup.setBackgroundDrawable(getResources().getDrawable(id));
    356     }
    357 
    358     /**
    359      * <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
    360      *
    361      * @param offset the vertical offset
    362      */
    363     public void setDropDownVerticalOffset(int offset) {
    364         mDropDownVerticalOffset = offset;
    365     }
    366 
    367     /**
    368      * <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
    369      *
    370      * @return the vertical offset
    371      */
    372     public int getDropDownVerticalOffset() {
    373         return mDropDownVerticalOffset;
    374     }
    375 
    376     /**
    377      * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
    378      *
    379      * @param offset the horizontal offset
    380      */
    381     public void setDropDownHorizontalOffset(int offset) {
    382         mDropDownHorizontalOffset = offset;
    383     }
    384 
    385     /**
    386      * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
    387      *
    388      * @return the horizontal offset
    389      */
    390     public int getDropDownHorizontalOffset() {
    391         return mDropDownHorizontalOffset;
    392     }
    393 
    394      /**
    395      * <p>Sets the animation style of the auto-complete drop-down list.</p>
    396      *
    397      * <p>If the drop-down is showing, calling this method will take effect only
    398      * the next time the drop-down is shown.</p>
    399      *
    400      * @param animationStyle animation style to use when the drop-down appears
    401      *      and disappears.  Set to -1 for the default animation, 0 for no
    402      *      animation, or a resource identifier for an explicit animation.
    403      *
    404      * @hide Pending API council approval
    405      */
    406     public void setDropDownAnimationStyle(int animationStyle) {
    407         mPopup.setAnimationStyle(animationStyle);
    408     }
    409 
    410     /**
    411      * <p>Returns the animation style that is used when the drop-down list appears and disappears
    412      * </p>
    413      *
    414      * @return the animation style that is used when the drop-down list appears and disappears
    415      *
    416      * @hide Pending API council approval
    417      */
    418     public int getDropDownAnimationStyle() {
    419         return mPopup.getAnimationStyle();
    420     }
    421 
    422     /**
    423      * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
    424      *
    425      * @hide Pending API council approval
    426      */
    427     public boolean isDropDownAlwaysVisible() {
    428         return mDropDownAlwaysVisible;
    429     }
    430 
    431     /**
    432      * Sets whether the drop-down should remain visible as long as there is there is
    433      * {@link #enoughToFilter()}.  This is useful if an unknown number of results are expected
    434      * to show up in the adapter sometime in the future.
    435      *
    436      * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
    437      * of the size or content of the list.  {@link #getDropDownBackground()} will fill any space
    438      * that is not used by the list.
    439      *
    440      * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
    441      *
    442      * @hide Pending API council approval
    443      */
    444     public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
    445         mDropDownAlwaysVisible = dropDownAlwaysVisible;
    446     }
    447 
    448     /**
    449      * Checks whether the drop-down is dismissed when a suggestion is clicked.
    450      *
    451      * @hide Pending API council approval
    452      */
    453     public boolean isDropDownDismissedOnCompletion() {
    454         return mDropDownDismissedOnCompletion;
    455     }
    456 
    457     /**
    458      * Sets whether the drop-down is dismissed when a suggestion is clicked. This is
    459      * true by default.
    460      *
    461      * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
    462      *
    463      * @hide Pending API council approval
    464      */
    465     public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
    466         mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
    467     }
    468 
    469     /**
    470      * <p>Returns the number of characters the user must type before the drop
    471      * down list is shown.</p>
    472      *
    473      * @return the minimum number of characters to type to show the drop down
    474      *
    475      * @see #setThreshold(int)
    476      */
    477     public int getThreshold() {
    478         return mThreshold;
    479     }
    480 
    481     /**
    482      * <p>Specifies the minimum number of characters the user has to type in the
    483      * edit box before the drop down list is shown.</p>
    484      *
    485      * <p>When <code>threshold</code> is less than or equals 0, a threshold of
    486      * 1 is applied.</p>
    487      *
    488      * @param threshold the number of characters to type before the drop down
    489      *                  is shown
    490      *
    491      * @see #getThreshold()
    492      *
    493      * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
    494      */
    495     public void setThreshold(int threshold) {
    496         if (threshold <= 0) {
    497             threshold = 1;
    498         }
    499 
    500         mThreshold = threshold;
    501     }
    502 
    503     /**
    504      * <p>Sets the listener that will be notified when the user clicks an item
    505      * in the drop down list.</p>
    506      *
    507      * @param l the item click listener
    508      */
    509     public void setOnItemClickListener(AdapterView.OnItemClickListener l) {
    510         mItemClickListener = l;
    511     }
    512 
    513     /**
    514      * <p>Sets the listener that will be notified when the user selects an item
    515      * in the drop down list.</p>
    516      *
    517      * @param l the item selected listener
    518      */
    519     public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
    520         mItemSelectedListener = l;
    521     }
    522 
    523     /**
    524      * <p>Returns the listener that is notified whenever the user clicks an item
    525      * in the drop down list.</p>
    526      *
    527      * @return the item click listener
    528      *
    529      * @deprecated Use {@link #getOnItemClickListener()} intead
    530      */
    531     @Deprecated
    532     public AdapterView.OnItemClickListener getItemClickListener() {
    533         return mItemClickListener;
    534     }
    535 
    536     /**
    537      * <p>Returns the listener that is notified whenever the user selects an
    538      * item in the drop down list.</p>
    539      *
    540      * @return the item selected listener
    541      *
    542      * @deprecated Use {@link #getOnItemSelectedListener()} intead
    543      */
    544     @Deprecated
    545     public AdapterView.OnItemSelectedListener getItemSelectedListener() {
    546         return mItemSelectedListener;
    547     }
    548 
    549     /**
    550      * <p>Returns the listener that is notified whenever the user clicks an item
    551      * in the drop down list.</p>
    552      *
    553      * @return the item click listener
    554      */
    555     public AdapterView.OnItemClickListener getOnItemClickListener() {
    556         return mItemClickListener;
    557     }
    558 
    559     /**
    560      * <p>Returns the listener that is notified whenever the user selects an
    561      * item in the drop down list.</p>
    562      *
    563      * @return the item selected listener
    564      */
    565     public AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
    566         return mItemSelectedListener;
    567     }
    568 
    569     /**
    570      * <p>Returns a filterable list adapter used for auto completion.</p>
    571      *
    572      * @return a data adapter used for auto completion
    573      */
    574     public ListAdapter getAdapter() {
    575         return mAdapter;
    576     }
    577 
    578     /**
    579      * <p>Changes the list of data used for auto completion. The provided list
    580      * must be a filterable list adapter.</p>
    581      *
    582      * <p>The caller is still responsible for managing any resources used by the adapter.
    583      * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified.
    584      * A common case is the use of {@link android.widget.CursorAdapter}, which
    585      * contains a {@link android.database.Cursor} that must be closed.  This can be done
    586      * automatically (see
    587      * {@link android.app.Activity#startManagingCursor(android.database.Cursor)
    588      * startManagingCursor()}),
    589      * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p>
    590      *
    591      * @param adapter the adapter holding the auto completion data
    592      *
    593      * @see #getAdapter()
    594      * @see android.widget.Filterable
    595      * @see android.widget.ListAdapter
    596      */
    597     public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
    598         if (mObserver == null) {
    599             mObserver = new PopupDataSetObserver();
    600         } else if (mAdapter != null) {
    601             mAdapter.unregisterDataSetObserver(mObserver);
    602         }
    603         mAdapter = adapter;
    604         if (mAdapter != null) {
    605             //noinspection unchecked
    606             mFilter = ((Filterable) mAdapter).getFilter();
    607             adapter.registerDataSetObserver(mObserver);
    608         } else {
    609             mFilter = null;
    610         }
    611 
    612         if (mDropDownList != null) {
    613             mDropDownList.setAdapter(mAdapter);
    614         }
    615     }
    616 
    617     @Override
    618     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
    619         if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
    620                 && !mDropDownAlwaysVisible) {
    621             // special case for the back key, we do not even try to send it
    622             // to the drop down list but instead, consume it immediately
    623             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
    624                 getKeyDispatcherState().startTracking(event, this);
    625                 return true;
    626             } else if (event.getAction() == KeyEvent.ACTION_UP) {
    627                 getKeyDispatcherState().handleUpEvent(event);
    628                 if (event.isTracking() && !event.isCanceled()) {
    629                     dismissDropDown();
    630                     return true;
    631                 }
    632             }
    633         }
    634         return super.onKeyPreIme(keyCode, event);
    635     }
    636 
    637     @Override
    638     public boolean onKeyUp(int keyCode, KeyEvent event) {
    639         if (isPopupShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
    640             boolean consumed = mDropDownList.onKeyUp(keyCode, event);
    641             if (consumed) {
    642                 switch (keyCode) {
    643                     // if the list accepts the key events and the key event
    644                     // was a click, the text view gets the selected item
    645                     // from the drop down as its content
    646                     case KeyEvent.KEYCODE_ENTER:
    647                     case KeyEvent.KEYCODE_DPAD_CENTER:
    648                         performCompletion();
    649                         return true;
    650                 }
    651             }
    652         }
    653         return super.onKeyUp(keyCode, event);
    654     }
    655 
    656     @Override
    657     public boolean onKeyDown(int keyCode, KeyEvent event) {
    658         // when the drop down is shown, we drive it directly
    659         if (isPopupShowing()) {
    660             // the key events are forwarded to the list in the drop down view
    661             // note that ListView handles space but we don't want that to happen
    662             // also if selection is not currently in the drop down, then don't
    663             // let center or enter presses go there since that would cause it
    664             // to select one of its items
    665             if (keyCode != KeyEvent.KEYCODE_SPACE
    666                     && (mDropDownList.getSelectedItemPosition() >= 0
    667                             || (keyCode != KeyEvent.KEYCODE_ENTER
    668                                     && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) {
    669                 int curIndex = mDropDownList.getSelectedItemPosition();
    670                 boolean consumed;
    671 
    672                 final boolean below = !mPopup.isAboveAnchor();
    673 
    674                 final ListAdapter adapter = mAdapter;
    675 
    676                 boolean allEnabled;
    677                 int firstItem = Integer.MAX_VALUE;
    678                 int lastItem = Integer.MIN_VALUE;
    679 
    680                 if (adapter != null) {
    681                     allEnabled = adapter.areAllItemsEnabled();
    682                     firstItem = allEnabled ? 0 :
    683                             mDropDownList.lookForSelectablePosition(0, true);
    684                     lastItem = allEnabled ? adapter.getCount() - 1 :
    685                             mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
    686                 }
    687 
    688                 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
    689                         (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
    690                     // When the selection is at the top, we block the key
    691                     // event to prevent focus from moving.
    692                     clearListSelection();
    693                     mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
    694                     showDropDown();
    695                     return true;
    696                 } else {
    697                     // WARNING: Please read the comment where mListSelectionHidden
    698                     //          is declared
    699                     mDropDownList.mListSelectionHidden = false;
    700                 }
    701 
    702                 consumed = mDropDownList.onKeyDown(keyCode, event);
    703                 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
    704 
    705                 if (consumed) {
    706                     // If it handled the key event, then the user is
    707                     // navigating in the list, so we should put it in front.
    708                     mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
    709                     // Here's a little trick we need to do to make sure that
    710                     // the list view is actually showing its focus indicator,
    711                     // by ensuring it has focus and getting its window out
    712                     // of touch mode.
    713                     mDropDownList.requestFocusFromTouch();
    714                     showDropDown();
    715 
    716                     switch (keyCode) {
    717                         // avoid passing the focus from the text view to the
    718                         // next component
    719                         case KeyEvent.KEYCODE_ENTER:
    720                         case KeyEvent.KEYCODE_DPAD_CENTER:
    721                         case KeyEvent.KEYCODE_DPAD_DOWN:
    722                         case KeyEvent.KEYCODE_DPAD_UP:
    723                             return true;
    724                     }
    725                 } else {
    726                     if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
    727                         // when the selection is at the bottom, we block the
    728                         // event to avoid going to the next focusable widget
    729                         if (curIndex == lastItem) {
    730                             return true;
    731                         }
    732                     } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
    733                             curIndex == firstItem) {
    734                         return true;
    735                     }
    736                 }
    737             }
    738         } else {
    739             switch(keyCode) {
    740             case KeyEvent.KEYCODE_DPAD_DOWN:
    741                 performValidation();
    742             }
    743         }
    744 
    745         mLastKeyCode = keyCode;
    746         boolean handled = super.onKeyDown(keyCode, event);
    747         mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    748 
    749         if (handled && isPopupShowing() && mDropDownList != null) {
    750             clearListSelection();
    751         }
    752 
    753         return handled;
    754     }
    755 
    756     /**
    757      * Returns <code>true</code> if the amount of text in the field meets
    758      * or exceeds the {@link #getThreshold} requirement.  You can override
    759      * this to impose a different standard for when filtering will be
    760      * triggered.
    761      */
    762     public boolean enoughToFilter() {
    763         if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
    764                 + " threshold=" + mThreshold);
    765         return getText().length() >= mThreshold;
    766     }
    767 
    768     /**
    769      * This is used to watch for edits to the text view.  Note that we call
    770      * to methods on the auto complete text view class so that we can access
    771      * private vars without going through thunks.
    772      */
    773     private class MyWatcher implements TextWatcher {
    774         public void afterTextChanged(Editable s) {
    775             doAfterTextChanged();
    776         }
    777         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    778             doBeforeTextChanged();
    779         }
    780         public void onTextChanged(CharSequence s, int start, int before, int count) {
    781         }
    782     }
    783 
    784     void doBeforeTextChanged() {
    785         if (mBlockCompletion) return;
    786 
    787         // when text is changed, inserted or deleted, we attempt to show
    788         // the drop down
    789         mOpenBefore = isPopupShowing();
    790         if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore);
    791     }
    792 
    793     void doAfterTextChanged() {
    794         if (mBlockCompletion) return;
    795 
    796         // if the list was open before the keystroke, but closed afterwards,
    797         // then something in the keystroke processing (an input filter perhaps)
    798         // called performCompletion() and we shouldn't do any more processing.
    799         if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore
    800                 + " open=" + isPopupShowing());
    801         if (mOpenBefore && !isPopupShowing()) {
    802             return;
    803         }
    804 
    805         // the drop down is shown only when a minimum number of characters
    806         // was typed in the text view
    807         if (enoughToFilter()) {
    808             if (mFilter != null) {
    809                 performFiltering(getText(), mLastKeyCode);
    810             }
    811         } else {
    812             // drop down is automatically dismissed when enough characters
    813             // are deleted from the text view
    814             if (!mDropDownAlwaysVisible) dismissDropDown();
    815             if (mFilter != null) {
    816                 mFilter.filter(null);
    817             }
    818         }
    819     }
    820 
    821     /**
    822      * <p>Indicates whether the popup menu is showing.</p>
    823      *
    824      * @return true if the popup menu is showing, false otherwise
    825      */
    826     public boolean isPopupShowing() {
    827         return mPopup.isShowing();
    828     }
    829 
    830     /**
    831      * <p>Converts the selected item from the drop down list into a sequence
    832      * of character that can be used in the edit box.</p>
    833      *
    834      * @param selectedItem the item selected by the user for completion
    835      *
    836      * @return a sequence of characters representing the selected suggestion
    837      */
    838     protected CharSequence convertSelectionToString(Object selectedItem) {
    839         return mFilter.convertResultToString(selectedItem);
    840     }
    841 
    842     /**
    843      * <p>Clear the list selection.  This may only be temporary, as user input will often bring
    844      * it back.
    845      */
    846     public void clearListSelection() {
    847         final DropDownListView list = mDropDownList;
    848         if (list != null) {
    849             // WARNING: Please read the comment where mListSelectionHidden is declared
    850             list.mListSelectionHidden = true;
    851             list.hideSelector();
    852             list.requestLayout();
    853         }
    854     }
    855 
    856     /**
    857      * Set the position of the dropdown view selection.
    858      *
    859      * @param position The position to move the selector to.
    860      */
    861     public void setListSelection(int position) {
    862         if (mPopup.isShowing() && (mDropDownList != null)) {
    863             mDropDownList.mListSelectionHidden = false;
    864             mDropDownList.setSelection(position);
    865             // ListView.setSelection() will call requestLayout()
    866         }
    867     }
    868 
    869     /**
    870      * Get the position of the dropdown view selection, if there is one.  Returns
    871      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
    872      * there is no selection.
    873      *
    874      * @return the position of the current selection, if there is one, or
    875      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
    876      *
    877      * @see ListView#getSelectedItemPosition()
    878      */
    879     public int getListSelection() {
    880         if (mPopup.isShowing() && (mDropDownList != null)) {
    881             return mDropDownList.getSelectedItemPosition();
    882         }
    883         return ListView.INVALID_POSITION;
    884     }
    885 
    886     /**
    887      * <p>Starts filtering the content of the drop down list. The filtering
    888      * pattern is the content of the edit box. Subclasses should override this
    889      * method to filter with a different pattern, for instance a substring of
    890      * <code>text</code>.</p>
    891      *
    892      * @param text the filtering pattern
    893      * @param keyCode the last character inserted in the edit box; beware that
    894      * this will be null when text is being added through a soft input method.
    895      */
    896     @SuppressWarnings({ "UnusedDeclaration" })
    897     protected void performFiltering(CharSequence text, int keyCode) {
    898         mFilter.filter(text, this);
    899     }
    900 
    901     /**
    902      * <p>Performs the text completion by converting the selected item from
    903      * the drop down list into a string, replacing the text box's content with
    904      * this string and finally dismissing the drop down menu.</p>
    905      */
    906     public void performCompletion() {
    907         performCompletion(null, -1, -1);
    908     }
    909 
    910     @Override
    911     public void onCommitCompletion(CompletionInfo completion) {
    912         if (isPopupShowing()) {
    913             mBlockCompletion = true;
    914             replaceText(completion.getText());
    915             mBlockCompletion = false;
    916 
    917             if (mItemClickListener != null) {
    918                 final DropDownListView list = mDropDownList;
    919                 // Note that we don't have a View here, so we will need to
    920                 // supply null.  Hopefully no existing apps crash...
    921                 mItemClickListener.onItemClick(list, null, completion.getPosition(),
    922                         completion.getId());
    923             }
    924         }
    925     }
    926 
    927     private void performCompletion(View selectedView, int position, long id) {
    928         if (isPopupShowing()) {
    929             Object selectedItem;
    930             if (position < 0) {
    931                 selectedItem = mDropDownList.getSelectedItem();
    932             } else {
    933                 selectedItem = mAdapter.getItem(position);
    934             }
    935             if (selectedItem == null) {
    936                 Log.w(TAG, "performCompletion: no selected item");
    937                 return;
    938             }
    939 
    940             mBlockCompletion = true;
    941             replaceText(convertSelectionToString(selectedItem));
    942             mBlockCompletion = false;
    943 
    944             if (mItemClickListener != null) {
    945                 final DropDownListView list = mDropDownList;
    946 
    947                 if (selectedView == null || position < 0) {
    948                     selectedView = list.getSelectedView();
    949                     position = list.getSelectedItemPosition();
    950                     id = list.getSelectedItemId();
    951                 }
    952                 mItemClickListener.onItemClick(list, selectedView, position, id);
    953             }
    954         }
    955 
    956         if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) {
    957             dismissDropDown();
    958         }
    959     }
    960 
    961     /**
    962      * Identifies whether the view is currently performing a text completion, so subclasses
    963      * can decide whether to respond to text changed events.
    964      */
    965     public boolean isPerformingCompletion() {
    966         return mBlockCompletion;
    967     }
    968 
    969     /**
    970      * Like {@link #setText(CharSequence)}, except that it can disable filtering.
    971      *
    972      * @param filter If <code>false</code>, no filtering will be performed
    973      *        as a result of this call.
    974      *
    975      * @hide Pending API council approval.
    976      */
    977     public void setText(CharSequence text, boolean filter) {
    978         if (filter) {
    979             setText(text);
    980         } else {
    981             mBlockCompletion = true;
    982             setText(text);
    983             mBlockCompletion = false;
    984         }
    985     }
    986 
    987     /**
    988      * <p>Performs the text completion by replacing the current text by the
    989      * selected item. Subclasses should override this method to avoid replacing
    990      * the whole content of the edit box.</p>
    991      *
    992      * @param text the selected suggestion in the drop down list
    993      */
    994     protected void replaceText(CharSequence text) {
    995         clearComposingText();
    996 
    997         setText(text);
    998         // make sure we keep the caret at the end of the text view
    999         Editable spannable = getText();
   1000         Selection.setSelection(spannable, spannable.length());
   1001     }
   1002 
   1003     /** {@inheritDoc} */
   1004     public void onFilterComplete(int count) {
   1005         updateDropDownForFilter(count);
   1006 
   1007     }
   1008 
   1009     private void updateDropDownForFilter(int count) {
   1010         // Not attached to window, don't update drop-down
   1011         if (getWindowVisibility() == View.GONE) return;
   1012 
   1013         /*
   1014          * This checks enoughToFilter() again because filtering requests
   1015          * are asynchronous, so the result may come back after enough text
   1016          * has since been deleted to make it no longer appropriate
   1017          * to filter.
   1018          */
   1019 
   1020         if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) {
   1021             if (hasFocus() && hasWindowFocus()) {
   1022                 showDropDown();
   1023             }
   1024         } else if (!mDropDownAlwaysVisible) {
   1025             dismissDropDown();
   1026         }
   1027     }
   1028 
   1029     @Override
   1030     public void onWindowFocusChanged(boolean hasWindowFocus) {
   1031         super.onWindowFocusChanged(hasWindowFocus);
   1032         if (!hasWindowFocus && !mDropDownAlwaysVisible) {
   1033             dismissDropDown();
   1034         }
   1035     }
   1036 
   1037     @Override
   1038     protected void onDisplayHint(int hint) {
   1039         super.onDisplayHint(hint);
   1040         switch (hint) {
   1041             case INVISIBLE:
   1042                 if (!mDropDownAlwaysVisible) {
   1043                     dismissDropDown();
   1044                 }
   1045                 break;
   1046         }
   1047     }
   1048 
   1049     @Override
   1050     protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
   1051         super.onFocusChanged(focused, direction, previouslyFocusedRect);
   1052         // Perform validation if the view is losing focus.
   1053         if (!focused) {
   1054             performValidation();
   1055         }
   1056         if (!focused && !mDropDownAlwaysVisible) {
   1057             dismissDropDown();
   1058         }
   1059     }
   1060 
   1061     @Override
   1062     protected void onAttachedToWindow() {
   1063         super.onAttachedToWindow();
   1064     }
   1065 
   1066     @Override
   1067     protected void onDetachedFromWindow() {
   1068         dismissDropDown();
   1069         super.onDetachedFromWindow();
   1070     }
   1071 
   1072     /**
   1073      * <p>Closes the drop down if present on screen.</p>
   1074      */
   1075     public void dismissDropDown() {
   1076         InputMethodManager imm = InputMethodManager.peekInstance();
   1077         if (imm != null) {
   1078             imm.displayCompletions(this, null);
   1079         }
   1080         mPopup.dismiss();
   1081         mPopup.setContentView(null);
   1082         mDropDownList = null;
   1083     }
   1084 
   1085     @Override
   1086     protected boolean setFrame(final int l, int t, final int r, int b) {
   1087         boolean result = super.setFrame(l, t, r, b);
   1088 
   1089         if (mPopup.isShowing()) {
   1090             showDropDown();
   1091         }
   1092 
   1093         return result;
   1094     }
   1095 
   1096     /**
   1097      * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of
   1098      * the id is NO_ID or we can't find a view for the given id, we return this TextView as
   1099      * the default anchoring point.</p>
   1100      */
   1101     private View getDropDownAnchorView() {
   1102         if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) {
   1103             mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId);
   1104         }
   1105         return mDropDownAnchorView == null ? this : mDropDownAnchorView;
   1106     }
   1107 
   1108     /**
   1109      * Issues a runnable to show the dropdown as soon as possible.
   1110      *
   1111      * @hide internal used only by SearchDialog
   1112      */
   1113     public void showDropDownAfterLayout() {
   1114         post(mShowDropDownRunnable);
   1115     }
   1116 
   1117     /**
   1118      * Ensures that the drop down is not obscuring the IME.
   1119      * @param visible whether the ime should be in front. If false, the ime is pushed to
   1120      * the background.
   1121      * @hide internal used only here and SearchDialog
   1122      */
   1123     public void ensureImeVisible(boolean visible) {
   1124         mPopup.setInputMethodMode(visible
   1125                 ? PopupWindow.INPUT_METHOD_NEEDED : PopupWindow.INPUT_METHOD_NOT_NEEDED);
   1126         showDropDown();
   1127     }
   1128 
   1129     /**
   1130      * @hide internal used only here and SearchDialog
   1131      */
   1132     public boolean isInputMethodNotNeeded() {
   1133         return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
   1134     }
   1135 
   1136     /**
   1137      * <p>Displays the drop down on screen.</p>
   1138      */
   1139     public void showDropDown() {
   1140         int height = buildDropDown();
   1141 
   1142         int widthSpec = 0;
   1143         int heightSpec = 0;
   1144 
   1145         boolean noInputMethod = isInputMethodNotNeeded();
   1146 
   1147         if (mPopup.isShowing()) {
   1148             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
   1149                 // The call to PopupWindow's update method below can accept -1 for any
   1150                 // value you do not want to update.
   1151                 widthSpec = -1;
   1152             } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
   1153                 widthSpec = getDropDownAnchorView().getWidth();
   1154             } else {
   1155                 widthSpec = mDropDownWidth;
   1156             }
   1157 
   1158             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
   1159                 // The call to PopupWindow's update method below can accept -1 for any
   1160                 // value you do not want to update.
   1161                 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
   1162                 if (noInputMethod) {
   1163                     mPopup.setWindowLayoutMode(
   1164                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
   1165                                     ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
   1166                 } else {
   1167                     mPopup.setWindowLayoutMode(
   1168                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
   1169                                     ViewGroup.LayoutParams.MATCH_PARENT : 0,
   1170                             ViewGroup.LayoutParams.MATCH_PARENT);
   1171                 }
   1172             } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
   1173                 heightSpec = height;
   1174             } else {
   1175                 heightSpec = mDropDownHeight;
   1176             }
   1177 
   1178             mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
   1179 
   1180             mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset,
   1181                     mDropDownVerticalOffset, widthSpec, heightSpec);
   1182         } else {
   1183             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
   1184                 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
   1185             } else {
   1186                 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
   1187                     mPopup.setWidth(getDropDownAnchorView().getWidth());
   1188                 } else {
   1189                     mPopup.setWidth(mDropDownWidth);
   1190                 }
   1191             }
   1192 
   1193             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
   1194                 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
   1195             } else {
   1196                 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
   1197                     mPopup.setHeight(height);
   1198                 } else {
   1199                     mPopup.setHeight(mDropDownHeight);
   1200                 }
   1201             }
   1202 
   1203             mPopup.setWindowLayoutMode(widthSpec, heightSpec);
   1204             mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
   1205 
   1206             // use outside touchable to dismiss drop down when touching outside of it, so
   1207             // only set this if the dropdown is not always visible
   1208             mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
   1209             mPopup.setTouchInterceptor(new PopupTouchInterceptor());
   1210             mPopup.showAsDropDown(getDropDownAnchorView(),
   1211                     mDropDownHorizontalOffset, mDropDownVerticalOffset);
   1212             mDropDownList.setSelection(ListView.INVALID_POSITION);
   1213             clearListSelection();
   1214             post(mHideSelector);
   1215         }
   1216     }
   1217 
   1218     /**
   1219      * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
   1220      * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
   1221      * ignore outside touch even when the drop down is not set to always visible.
   1222      *
   1223      * @hide used only by SearchDialog
   1224      */
   1225     public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
   1226         mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
   1227     }
   1228 
   1229     /**
   1230      * <p>Builds the popup window's content and returns the height the popup
   1231      * should have. Returns -1 when the content already exists.</p>
   1232      *
   1233      * @return the content's height or -1 if content already exists
   1234      */
   1235     private int buildDropDown() {
   1236         ViewGroup dropDownView;
   1237         int otherHeights = 0;
   1238 
   1239         final ListAdapter adapter = mAdapter;
   1240         if (adapter != null) {
   1241             InputMethodManager imm = InputMethodManager.peekInstance();
   1242             if (imm != null) {
   1243                 final int count = Math.min(adapter.getCount(), 20);
   1244                 CompletionInfo[] completions = new CompletionInfo[count];
   1245                 int realCount = 0;
   1246 
   1247                 for (int i = 0; i < count; i++) {
   1248                     if (adapter.isEnabled(i)) {
   1249                         realCount++;
   1250                         Object item = adapter.getItem(i);
   1251                         long id = adapter.getItemId(i);
   1252                         completions[i] = new CompletionInfo(id, i,
   1253                                 convertSelectionToString(item));
   1254                     }
   1255                 }
   1256 
   1257                 if (realCount != count) {
   1258                     CompletionInfo[] tmp = new CompletionInfo[realCount];
   1259                     System.arraycopy(completions, 0, tmp, 0, realCount);
   1260                     completions = tmp;
   1261                 }
   1262 
   1263                 imm.displayCompletions(this, completions);
   1264             }
   1265         }
   1266 
   1267         if (mDropDownList == null) {
   1268             Context context = getContext();
   1269 
   1270             mHideSelector = new ListSelectorHider();
   1271 
   1272             /**
   1273              * This Runnable exists for the sole purpose of checking if the view layout has got
   1274              * completed and if so call showDropDown to display the drop down. This is used to show
   1275              * the drop down as soon as possible after user opens up the search dialog, without
   1276              * waiting for the normal UI pipeline to do it's job which is slower than this method.
   1277              */
   1278             mShowDropDownRunnable = new Runnable() {
   1279                 public void run() {
   1280                     // View layout should be all done before displaying the drop down.
   1281                     View view = getDropDownAnchorView();
   1282                     if (view != null && view.getWindowToken() != null) {
   1283                         showDropDown();
   1284                     }
   1285                 }
   1286             };
   1287 
   1288             mDropDownList = new DropDownListView(context);
   1289             mDropDownList.setSelector(mDropDownListHighlight);
   1290             mDropDownList.setAdapter(adapter);
   1291             mDropDownList.setVerticalFadingEdgeEnabled(true);
   1292             mDropDownList.setOnItemClickListener(mDropDownItemClickListener);
   1293             mDropDownList.setFocusable(true);
   1294             mDropDownList.setFocusableInTouchMode(true);
   1295             mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
   1296                 public void onItemSelected(AdapterView<?> parent, View view,
   1297                         int position, long id) {
   1298 
   1299                     if (position != -1) {
   1300                         DropDownListView dropDownList = mDropDownList;
   1301 
   1302                         if (dropDownList != null) {
   1303                             dropDownList.mListSelectionHidden = false;
   1304                         }
   1305                     }
   1306                 }
   1307 
   1308                 public void onNothingSelected(AdapterView<?> parent) {
   1309                 }
   1310             });
   1311             mDropDownList.setOnScrollListener(new PopupScrollListener());
   1312 
   1313             if (mItemSelectedListener != null) {
   1314                 mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
   1315             }
   1316 
   1317             dropDownView = mDropDownList;
   1318 
   1319             View hintView = getHintView(context);
   1320             if (hintView != null) {
   1321                 // if an hint has been specified, we accomodate more space for it and
   1322                 // add a text view in the drop down menu, at the bottom of the list
   1323                 LinearLayout hintContainer = new LinearLayout(context);
   1324                 hintContainer.setOrientation(LinearLayout.VERTICAL);
   1325 
   1326                 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
   1327                         ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
   1328                 );
   1329                 hintContainer.addView(dropDownView, hintParams);
   1330                 hintContainer.addView(hintView);
   1331 
   1332                 // measure the hint's height to find how much more vertical space
   1333                 // we need to add to the drop down's height
   1334                 int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST);
   1335                 int heightSpec = MeasureSpec.UNSPECIFIED;
   1336                 hintView.measure(widthSpec, heightSpec);
   1337 
   1338                 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
   1339                 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
   1340                         + hintParams.bottomMargin;
   1341 
   1342                 dropDownView = hintContainer;
   1343             }
   1344 
   1345             mPopup.setContentView(dropDownView);
   1346         } else {
   1347             dropDownView = (ViewGroup) mPopup.getContentView();
   1348             final View view = dropDownView.findViewById(HINT_VIEW_ID);
   1349             if (view != null) {
   1350                 LinearLayout.LayoutParams hintParams =
   1351                         (LinearLayout.LayoutParams) view.getLayoutParams();
   1352                 otherHeights = view.getMeasuredHeight() + hintParams.topMargin
   1353                         + hintParams.bottomMargin;
   1354             }
   1355         }
   1356 
   1357         // Max height available on the screen for a popup.
   1358         boolean ignoreBottomDecorations =
   1359                 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
   1360         final int maxHeight = mPopup.getMaxAvailableHeight(
   1361                 getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
   1362 
   1363         // getMaxAvailableHeight() subtracts the padding, so we put it back,
   1364         // to get the available height for the whole window
   1365         int padding = 0;
   1366         Drawable background = mPopup.getBackground();
   1367         if (background != null) {
   1368             background.getPadding(mTempRect);
   1369             padding = mTempRect.top + mTempRect.bottom;
   1370         }
   1371 
   1372         if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
   1373             return maxHeight + padding;
   1374         }
   1375 
   1376         final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
   1377                 0, ListView.NO_POSITION, maxHeight - otherHeights, 2);
   1378         // add padding only if the list has items in it, that way we don't show
   1379         // the popup if it is not needed
   1380         if (listContent > 0) otherHeights += padding;
   1381 
   1382         return listContent + otherHeights;
   1383     }
   1384 
   1385     private View getHintView(Context context) {
   1386         if (mHintText != null && mHintText.length() > 0) {
   1387             final TextView hintView = (TextView) LayoutInflater.from(context).inflate(
   1388                     mHintResource, null).findViewById(com.android.internal.R.id.text1);
   1389             hintView.setText(mHintText);
   1390             hintView.setId(HINT_VIEW_ID);
   1391             return hintView;
   1392         } else {
   1393             return null;
   1394         }
   1395     }
   1396 
   1397     /**
   1398      * Sets the validator used to perform text validation.
   1399      *
   1400      * @param validator The validator used to validate the text entered in this widget.
   1401      *
   1402      * @see #getValidator()
   1403      * @see #performValidation()
   1404      */
   1405     public void setValidator(Validator validator) {
   1406         mValidator = validator;
   1407     }
   1408 
   1409     /**
   1410      * Returns the Validator set with {@link #setValidator},
   1411      * or <code>null</code> if it was not set.
   1412      *
   1413      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
   1414      * @see #performValidation()
   1415      */
   1416     public Validator getValidator() {
   1417         return mValidator;
   1418     }
   1419 
   1420     /**
   1421      * If a validator was set on this view and the current string is not valid,
   1422      * ask the validator to fix it.
   1423      *
   1424      * @see #getValidator()
   1425      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
   1426      */
   1427     public void performValidation() {
   1428         if (mValidator == null) return;
   1429 
   1430         CharSequence text = getText();
   1431 
   1432         if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) {
   1433             setText(mValidator.fixText(text));
   1434         }
   1435     }
   1436 
   1437     /**
   1438      * Returns the Filter obtained from {@link Filterable#getFilter},
   1439      * or <code>null</code> if {@link #setAdapter} was not called with
   1440      * a Filterable.
   1441      */
   1442     protected Filter getFilter() {
   1443         return mFilter;
   1444     }
   1445 
   1446     private class ListSelectorHider implements Runnable {
   1447         public void run() {
   1448             clearListSelection();
   1449         }
   1450     }
   1451 
   1452     private class ResizePopupRunnable implements Runnable {
   1453         public void run() {
   1454             mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
   1455             showDropDown();
   1456         }
   1457     }
   1458 
   1459     private class PopupTouchInterceptor implements OnTouchListener {
   1460         public boolean onTouch(View v, MotionEvent event) {
   1461             final int action = event.getAction();
   1462             if (action == MotionEvent.ACTION_DOWN &&
   1463                     mPopup != null && mPopup.isShowing()) {
   1464                 postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
   1465             } else if (action == MotionEvent.ACTION_UP) {
   1466                 removeCallbacks(mResizePopupRunnable);
   1467             }
   1468             return false;
   1469         }
   1470     }
   1471 
   1472     private class PopupScrollListener implements ListView.OnScrollListener {
   1473         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
   1474                 int totalItemCount) {
   1475 
   1476         }
   1477 
   1478         public void onScrollStateChanged(AbsListView view, int scrollState) {
   1479             if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
   1480                     !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
   1481                 removeCallbacks(mResizePopupRunnable);
   1482                 mResizePopupRunnable.run();
   1483             }
   1484         }
   1485     }
   1486 
   1487     private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
   1488         public void onItemClick(AdapterView parent, View v, int position, long id) {
   1489             performCompletion(v, position, id);
   1490         }
   1491     }
   1492 
   1493     /**
   1494      * <p>Wrapper class for a ListView. This wrapper hijacks the focus to
   1495      * make sure the list uses the appropriate drawables and states when
   1496      * displayed on screen within a drop down. The focus is never actually
   1497      * passed to the drop down; the list only looks focused.</p>
   1498      */
   1499     private static class DropDownListView extends ListView {
   1500         /*
   1501          * WARNING: This is a workaround for a touch mode issue.
   1502          *
   1503          * Touch mode is propagated lazily to windows. This causes problems in
   1504          * the following scenario:
   1505          * - Type something in the AutoCompleteTextView and get some results
   1506          * - Move down with the d-pad to select an item in the list
   1507          * - Move up with the d-pad until the selection disappears
   1508          * - Type more text in the AutoCompleteTextView *using the soft keyboard*
   1509          *   and get new results; you are now in touch mode
   1510          * - The selection comes back on the first item in the list, even though
   1511          *   the list is supposed to be in touch mode
   1512          *
   1513          * Using the soft keyboard triggers the touch mode change but that change
   1514          * is propagated to our window only after the first list layout, therefore
   1515          * after the list attempts to resurrect the selection.
   1516          *
   1517          * The trick to work around this issue is to pretend the list is in touch
   1518          * mode when we know that the selection should not appear, that is when
   1519          * we know the user moved the selection away from the list.
   1520          *
   1521          * This boolean is set to true whenever we explicitely hide the list's
   1522          * selection and reset to false whenver we know the user moved the
   1523          * selection back to the list.
   1524          *
   1525          * When this boolean is true, isInTouchMode() returns true, otherwise it
   1526          * returns super.isInTouchMode().
   1527          */
   1528         private boolean mListSelectionHidden;
   1529 
   1530         /**
   1531          * <p>Creates a new list view wrapper.</p>
   1532          *
   1533          * @param context this view's context
   1534          */
   1535         public DropDownListView(Context context) {
   1536             super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
   1537         }
   1538 
   1539         /**
   1540          * <p>Avoids jarring scrolling effect by ensuring that list elements
   1541          * made of a text view fit on a single line.</p>
   1542          *
   1543          * @param position the item index in the list to get a view for
   1544          * @return the view for the specified item
   1545          */
   1546         @Override
   1547         View obtainView(int position, boolean[] isScrap) {
   1548             View view = super.obtainView(position, isScrap);
   1549 
   1550             if (view instanceof TextView) {
   1551                 ((TextView) view).setHorizontallyScrolling(true);
   1552             }
   1553 
   1554             return view;
   1555         }
   1556 
   1557         @Override
   1558         public boolean isInTouchMode() {
   1559             // WARNING: Please read the comment where mListSelectionHidden is declared
   1560             return mListSelectionHidden || super.isInTouchMode();
   1561         }
   1562 
   1563         /**
   1564          * <p>Returns the focus state in the drop down.</p>
   1565          *
   1566          * @return true always
   1567          */
   1568         @Override
   1569         public boolean hasWindowFocus() {
   1570             return true;
   1571         }
   1572 
   1573         /**
   1574          * <p>Returns the focus state in the drop down.</p>
   1575          *
   1576          * @return true always
   1577          */
   1578         @Override
   1579         public boolean isFocused() {
   1580             return true;
   1581         }
   1582 
   1583         /**
   1584          * <p>Returns the focus state in the drop down.</p>
   1585          *
   1586          * @return true always
   1587          */
   1588         @Override
   1589         public boolean hasFocus() {
   1590             return true;
   1591         }
   1592 
   1593         protected int[] onCreateDrawableState(int extraSpace) {
   1594             int[] res = super.onCreateDrawableState(extraSpace);
   1595             //noinspection ConstantIfStatement
   1596             if (false) {
   1597                 StringBuilder sb = new StringBuilder("Created drawable state: [");
   1598                 for (int i=0; i<res.length; i++) {
   1599                     if (i > 0) sb.append(", ");
   1600                     sb.append("0x");
   1601                     sb.append(Integer.toHexString(res[i]));
   1602                 }
   1603                 sb.append("]");
   1604                 Log.i(TAG, sb.toString());
   1605             }
   1606             return res;
   1607         }
   1608     }
   1609 
   1610     /**
   1611      * This interface is used to make sure that the text entered in this TextView complies to
   1612      * a certain format.  Since there is no foolproof way to prevent the user from leaving
   1613      * this View with an incorrect value in it, all we can do is try to fix it ourselves
   1614      * when this happens.
   1615      */
   1616     public interface Validator {
   1617         /**
   1618          * Validates the specified text.
   1619          *
   1620          * @return true If the text currently in the text editor is valid.
   1621          *
   1622          * @see #fixText(CharSequence)
   1623          */
   1624         boolean isValid(CharSequence text);
   1625 
   1626         /**
   1627          * Corrects the specified text to make it valid.
   1628          *
   1629          * @param invalidText A string that doesn't pass validation: isValid(invalidText)
   1630          *        returns false
   1631          *
   1632          * @return A string based on invalidText such as invoking isValid() on it returns true.
   1633          *
   1634          * @see #isValid(CharSequence)
   1635          */
   1636         CharSequence fixText(CharSequence invalidText);
   1637     }
   1638 
   1639     /**
   1640      * Allows us a private hook into the on click event without preventing users from setting
   1641      * their own click listener.
   1642      */
   1643     private class PassThroughClickListener implements OnClickListener {
   1644 
   1645         private View.OnClickListener mWrapped;
   1646 
   1647         /** {@inheritDoc} */
   1648         public void onClick(View v) {
   1649             onClickImpl();
   1650 
   1651             if (mWrapped != null) mWrapped.onClick(v);
   1652         }
   1653     }
   1654 
   1655     private class PopupDataSetObserver extends DataSetObserver {
   1656         @Override
   1657         public void onChanged() {
   1658             if (isPopupShowing()) {
   1659                 // This will resize the popup to fit the new adapter's content
   1660                 showDropDown();
   1661             } else if (mAdapter != null) {
   1662                 // If the popup is not showing already, showing it will cause
   1663                 // the list of data set observers attached to the adapter to
   1664                 // change. We can't do it from here, because we are in the middle
   1665                 // of iterating throught he list of observers.
   1666                 post(new Runnable() {
   1667                     public void run() {
   1668                         final ListAdapter adapter = mAdapter;
   1669                         if (adapter != null) {
   1670                             updateDropDownForFilter(adapter.getCount());
   1671                         }
   1672                     }
   1673                 });
   1674             }
   1675         }
   1676 
   1677         @Override
   1678         public void onInvalidated() {
   1679             if (!mDropDownAlwaysVisible) {
   1680                 // There's no data to display so make sure we're not showing
   1681                 // the drop down and its list
   1682                 dismissDropDown();
   1683             }
   1684         }
   1685     }
   1686 }
   1687