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