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.TypedArray;
21import android.database.DataSetObserver;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.text.Editable;
25import android.text.Selection;
26import android.text.TextUtils;
27import android.text.TextWatcher;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.KeyEvent;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.WindowManager;
35import android.view.inputmethod.CompletionInfo;
36import android.view.inputmethod.EditorInfo;
37import android.view.inputmethod.InputMethodManager;
38
39import com.android.internal.R;
40
41
42/**
43 * <p>An editable text view that shows completion suggestions automatically
44 * while the user is typing. The list of suggestions is displayed in a drop
45 * down menu from which the user can choose an item to replace the content
46 * of the edit box with.</p>
47 *
48 * <p>The drop down can be dismissed at any time by pressing the back key or,
49 * if no item is selected in the drop down, by pressing the enter/dpad center
50 * key.</p>
51 *
52 * <p>The list of suggestions is obtained from a data adapter and appears
53 * only after a given number of characters defined by
54 * {@link #getThreshold() the threshold}.</p>
55 *
56 * <p>The following code snippet shows how to create a text view which suggests
57 * various countries names while the user is typing:</p>
58 *
59 * <pre class="prettyprint">
60 * public class CountriesActivity extends Activity {
61 *     protected void onCreate(Bundle icicle) {
62 *         super.onCreate(icicle);
63 *         setContentView(R.layout.countries);
64 *
65 *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
66 *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
67 *         AutoCompleteTextView textView = (AutoCompleteTextView)
68 *                 findViewById(R.id.countries_list);
69 *         textView.setAdapter(adapter);
70 *     }
71 *
72 *     private static final String[] COUNTRIES = new String[] {
73 *         "Belgium", "France", "Italy", "Germany", "Spain"
74 *     };
75 * }
76 * </pre>
77 *
78 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a>
79 * guide.</p>
80 *
81 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
82 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
83 * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView
84 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector
85 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
86 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
87 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
88 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownVerticalOffset
89 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHorizontalOffset
90 */
91public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
92    static final boolean DEBUG = false;
93    static final String TAG = "AutoCompleteTextView";
94
95    static final int EXPAND_MAX = 3;
96
97    private CharSequence mHintText;
98    private TextView mHintView;
99    private int mHintResource;
100
101    private ListAdapter mAdapter;
102    private Filter mFilter;
103    private int mThreshold;
104
105    private ListPopupWindow mPopup;
106    private int mDropDownAnchorId;
107
108    private AdapterView.OnItemClickListener mItemClickListener;
109    private AdapterView.OnItemSelectedListener mItemSelectedListener;
110
111    private boolean mDropDownDismissedOnCompletion = true;
112
113    private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
114    private boolean mOpenBefore;
115
116    private Validator mValidator = null;
117
118    // Set to true when text is set directly and no filtering shall be performed
119    private boolean mBlockCompletion;
120
121    // When set, an update in the underlying adapter will update the result list popup.
122    // Set to false when the list is hidden to prevent asynchronous updates to popup the list again.
123    private boolean mPopupCanBeUpdated = true;
124
125    private PassThroughClickListener mPassThroughClickListener;
126    private PopupDataSetObserver mObserver;
127
128    public AutoCompleteTextView(Context context) {
129        this(context, null);
130    }
131
132    public AutoCompleteTextView(Context context, AttributeSet attrs) {
133        this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
134    }
135
136    public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
137        super(context, attrs, defStyle);
138
139        mPopup = new ListPopupWindow(context, attrs,
140                com.android.internal.R.attr.autoCompleteTextViewStyle);
141        mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
142        mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW);
143
144        TypedArray a =
145            context.obtainStyledAttributes(
146                attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0);
147
148        mThreshold = a.getInt(
149                R.styleable.AutoCompleteTextView_completionThreshold, 2);
150
151        mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector));
152        mPopup.setVerticalOffset((int)
153                a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f));
154        mPopup.setHorizontalOffset((int)
155                a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f));
156
157        // Get the anchor's id now, but the view won't be ready, so wait to actually get the
158        // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later.
159        // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return
160        // this TextView, as a default anchoring point.
161        mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor,
162                View.NO_ID);
163
164        // For dropdown width, the developer can specify a specific width, or MATCH_PARENT
165        // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view).
166        mPopup.setWidth(a.getLayoutDimension(
167                R.styleable.AutoCompleteTextView_dropDownWidth,
168                ViewGroup.LayoutParams.WRAP_CONTENT));
169        mPopup.setHeight(a.getLayoutDimension(
170                R.styleable.AutoCompleteTextView_dropDownHeight,
171                ViewGroup.LayoutParams.WRAP_CONTENT));
172
173        mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
174                R.layout.simple_dropdown_hint);
175
176        mPopup.setOnItemClickListener(new DropDownItemClickListener());
177        setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint));
178
179        // Always turn on the auto complete input type flag, since it
180        // makes no sense to use this widget without it.
181        int inputType = getInputType();
182        if ((inputType&EditorInfo.TYPE_MASK_CLASS)
183                == EditorInfo.TYPE_CLASS_TEXT) {
184            inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
185            setRawInputType(inputType);
186        }
187
188        a.recycle();
189
190        setFocusable(true);
191
192        addTextChangedListener(new MyWatcher());
193
194        mPassThroughClickListener = new PassThroughClickListener();
195        super.setOnClickListener(mPassThroughClickListener);
196    }
197
198    @Override
199    public void setOnClickListener(OnClickListener listener) {
200        mPassThroughClickListener.mWrapped = listener;
201    }
202
203    /**
204     * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
205     */
206    private void onClickImpl() {
207        // If the dropdown is showing, bring the keyboard to the front
208        // when the user touches the text field.
209        if (isPopupShowing()) {
210            ensureImeVisible(true);
211        }
212    }
213
214    /**
215     * <p>Sets the optional hint text that is displayed at the bottom of the
216     * the matching list.  This can be used as a cue to the user on how to
217     * best use the list, or to provide extra information.</p>
218     *
219     * @param hint the text to be displayed to the user
220     *
221     * @see #getCompletionHint()
222     *
223     * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
224     */
225    public void setCompletionHint(CharSequence hint) {
226        mHintText = hint;
227        if (hint != null) {
228            if (mHintView == null) {
229                final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate(
230                        mHintResource, null).findViewById(com.android.internal.R.id.text1);
231                hintView.setText(mHintText);
232                mHintView = hintView;
233                mPopup.setPromptView(hintView);
234            } else {
235                mHintView.setText(hint);
236            }
237        } else {
238            mPopup.setPromptView(null);
239            mHintView = null;
240        }
241    }
242
243    /**
244     * Gets the optional hint text displayed at the bottom of the the matching list.
245     *
246     * @return The hint text, if any
247     *
248     * @see #setCompletionHint(CharSequence)
249     *
250     * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
251     */
252    public CharSequence getCompletionHint() {
253        return mHintText;
254    }
255
256    /**
257     * <p>Returns 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     * @return the width for the drop down list
262     *
263     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
264     */
265    public int getDropDownWidth() {
266        return mPopup.getWidth();
267    }
268
269    /**
270     * <p>Sets the current width for the auto-complete drop down list. This can
271     * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
272     * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
273     *
274     * @param width the width to use
275     *
276     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
277     */
278    public void setDropDownWidth(int width) {
279        mPopup.setWidth(width);
280    }
281
282    /**
283     * <p>Returns the current height for the auto-complete drop down list. This can
284     * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
285     * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
286     * of the drop down's content.</p>
287     *
288     * @return the height for the drop down list
289     *
290     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
291     */
292    public int getDropDownHeight() {
293        return mPopup.getHeight();
294    }
295
296    /**
297     * <p>Sets the current height for the auto-complete drop down list. This can
298     * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
299     * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
300     * of the drop down's content.</p>
301     *
302     * @param height the height to use
303     *
304     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
305     */
306    public void setDropDownHeight(int height) {
307        mPopup.setHeight(height);
308    }
309
310    /**
311     * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
312     *
313     * @return the view's id, or {@link View#NO_ID} if none specified
314     *
315     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
316     */
317    public int getDropDownAnchor() {
318        return mDropDownAnchorId;
319    }
320
321    /**
322     * <p>Sets the view to which the auto-complete drop down list should anchor. The view
323     * corresponding to this id will not be loaded until the next time it is needed to avoid
324     * loading a view which is not yet instantiated.</p>
325     *
326     * @param id the id to anchor the drop down list view to
327     *
328     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
329     */
330    public void setDropDownAnchor(int id) {
331        mDropDownAnchorId = id;
332        mPopup.setAnchorView(null);
333    }
334
335    /**
336     * <p>Gets the background of the auto-complete drop-down list.</p>
337     *
338     * @return the background drawable
339     *
340     * @attr ref android.R.styleable#PopupWindow_popupBackground
341     */
342    public Drawable getDropDownBackground() {
343        return mPopup.getBackground();
344    }
345
346    /**
347     * <p>Sets the background of the auto-complete drop-down list.</p>
348     *
349     * @param d the drawable to set as the background
350     *
351     * @attr ref android.R.styleable#PopupWindow_popupBackground
352     */
353    public void setDropDownBackgroundDrawable(Drawable d) {
354        mPopup.setBackgroundDrawable(d);
355    }
356
357    /**
358     * <p>Sets the background of the auto-complete drop-down list.</p>
359     *
360     * @param id the id of the drawable to set as the background
361     *
362     * @attr ref android.R.styleable#PopupWindow_popupBackground
363     */
364    public void setDropDownBackgroundResource(int id) {
365        mPopup.setBackgroundDrawable(getResources().getDrawable(id));
366    }
367
368    /**
369     * <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
370     *
371     * @param offset the vertical offset
372     */
373    public void setDropDownVerticalOffset(int offset) {
374        mPopup.setVerticalOffset(offset);
375    }
376
377    /**
378     * <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
379     *
380     * @return the vertical offset
381     */
382    public int getDropDownVerticalOffset() {
383        return mPopup.getVerticalOffset();
384    }
385
386    /**
387     * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
388     *
389     * @param offset the horizontal offset
390     */
391    public void setDropDownHorizontalOffset(int offset) {
392        mPopup.setHorizontalOffset(offset);
393    }
394
395    /**
396     * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
397     *
398     * @return the horizontal offset
399     */
400    public int getDropDownHorizontalOffset() {
401        return mPopup.getHorizontalOffset();
402    }
403
404     /**
405     * <p>Sets the animation style of the auto-complete drop-down list.</p>
406     *
407     * <p>If the drop-down is showing, calling this method will take effect only
408     * the next time the drop-down is shown.</p>
409     *
410     * @param animationStyle animation style to use when the drop-down appears
411     *      and disappears.  Set to -1 for the default animation, 0 for no
412     *      animation, or a resource identifier for an explicit animation.
413     *
414     * @hide Pending API council approval
415     */
416    public void setDropDownAnimationStyle(int animationStyle) {
417        mPopup.setAnimationStyle(animationStyle);
418    }
419
420    /**
421     * <p>Returns the animation style that is used when the drop-down list appears and disappears
422     * </p>
423     *
424     * @return the animation style that is used when the drop-down list appears and disappears
425     *
426     * @hide Pending API council approval
427     */
428    public int getDropDownAnimationStyle() {
429        return mPopup.getAnimationStyle();
430    }
431
432    /**
433     * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
434     *
435     * @hide Pending API council approval
436     */
437    public boolean isDropDownAlwaysVisible() {
438        return mPopup.isDropDownAlwaysVisible();
439    }
440
441    /**
442     * Sets whether the drop-down should remain visible as long as there is there is
443     * {@link #enoughToFilter()}.  This is useful if an unknown number of results are expected
444     * to show up in the adapter sometime in the future.
445     *
446     * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
447     * of the size or content of the list.  {@link #getDropDownBackground()} will fill any space
448     * that is not used by the list.
449     *
450     * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
451     *
452     * @hide Pending API council approval
453     */
454    public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
455        mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible);
456    }
457
458    /**
459     * Checks whether the drop-down is dismissed when a suggestion is clicked.
460     *
461     * @hide Pending API council approval
462     */
463    public boolean isDropDownDismissedOnCompletion() {
464        return mDropDownDismissedOnCompletion;
465    }
466
467    /**
468     * Sets whether the drop-down is dismissed when a suggestion is clicked. This is
469     * true by default.
470     *
471     * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
472     *
473     * @hide Pending API council approval
474     */
475    public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
476        mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
477    }
478
479    /**
480     * <p>Returns the number of characters the user must type before the drop
481     * down list is shown.</p>
482     *
483     * @return the minimum number of characters to type to show the drop down
484     *
485     * @see #setThreshold(int)
486     *
487     * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
488     */
489    public int getThreshold() {
490        return mThreshold;
491    }
492
493    /**
494     * <p>Specifies the minimum number of characters the user has to type in the
495     * edit box before the drop down list is shown.</p>
496     *
497     * <p>When <code>threshold</code> is less than or equals 0, a threshold of
498     * 1 is applied.</p>
499     *
500     * @param threshold the number of characters to type before the drop down
501     *                  is shown
502     *
503     * @see #getThreshold()
504     *
505     * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
506     */
507    public void setThreshold(int threshold) {
508        if (threshold <= 0) {
509            threshold = 1;
510        }
511
512        mThreshold = threshold;
513    }
514
515    /**
516     * <p>Sets the listener that will be notified when the user clicks an item
517     * in the drop down list.</p>
518     *
519     * @param l the item click listener
520     */
521    public void setOnItemClickListener(AdapterView.OnItemClickListener l) {
522        mItemClickListener = l;
523    }
524
525    /**
526     * <p>Sets the listener that will be notified when the user selects an item
527     * in the drop down list.</p>
528     *
529     * @param l the item selected listener
530     */
531    public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
532        mItemSelectedListener = l;
533    }
534
535    /**
536     * <p>Returns the listener that is notified whenever the user clicks an item
537     * in the drop down list.</p>
538     *
539     * @return the item click listener
540     *
541     * @deprecated Use {@link #getOnItemClickListener()} intead
542     */
543    @Deprecated
544    public AdapterView.OnItemClickListener getItemClickListener() {
545        return mItemClickListener;
546    }
547
548    /**
549     * <p>Returns the listener that is notified whenever the user selects an
550     * item in the drop down list.</p>
551     *
552     * @return the item selected listener
553     *
554     * @deprecated Use {@link #getOnItemSelectedListener()} intead
555     */
556    @Deprecated
557    public AdapterView.OnItemSelectedListener getItemSelectedListener() {
558        return mItemSelectedListener;
559    }
560
561    /**
562     * <p>Returns the listener that is notified whenever the user clicks an item
563     * in the drop down list.</p>
564     *
565     * @return the item click listener
566     */
567    public AdapterView.OnItemClickListener getOnItemClickListener() {
568        return mItemClickListener;
569    }
570
571    /**
572     * <p>Returns the listener that is notified whenever the user selects an
573     * item in the drop down list.</p>
574     *
575     * @return the item selected listener
576     */
577    public AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
578        return mItemSelectedListener;
579    }
580
581    /**
582     * Set a listener that will be invoked whenever the AutoCompleteTextView's
583     * list of completions is dismissed.
584     * @param dismissListener Listener to invoke when completions are dismissed
585     */
586    public void setOnDismissListener(final OnDismissListener dismissListener) {
587        PopupWindow.OnDismissListener wrappedListener = null;
588        if (dismissListener != null) {
589            wrappedListener = new PopupWindow.OnDismissListener() {
590                @Override public void onDismiss() {
591                    dismissListener.onDismiss();
592                }
593            };
594        }
595        mPopup.setOnDismissListener(wrappedListener);
596    }
597
598    /**
599     * <p>Returns a filterable list adapter used for auto completion.</p>
600     *
601     * @return a data adapter used for auto completion
602     */
603    public ListAdapter getAdapter() {
604        return mAdapter;
605    }
606
607    /**
608     * <p>Changes the list of data used for auto completion. The provided list
609     * must be a filterable list adapter.</p>
610     *
611     * <p>The caller is still responsible for managing any resources used by the adapter.
612     * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified.
613     * A common case is the use of {@link android.widget.CursorAdapter}, which
614     * contains a {@link android.database.Cursor} that must be closed.  This can be done
615     * automatically (see
616     * {@link android.app.Activity#startManagingCursor(android.database.Cursor)
617     * startManagingCursor()}),
618     * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p>
619     *
620     * @param adapter the adapter holding the auto completion data
621     *
622     * @see #getAdapter()
623     * @see android.widget.Filterable
624     * @see android.widget.ListAdapter
625     */
626    public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
627        if (mObserver == null) {
628            mObserver = new PopupDataSetObserver();
629        } else if (mAdapter != null) {
630            mAdapter.unregisterDataSetObserver(mObserver);
631        }
632        mAdapter = adapter;
633        if (mAdapter != null) {
634            //noinspection unchecked
635            mFilter = ((Filterable) mAdapter).getFilter();
636            adapter.registerDataSetObserver(mObserver);
637        } else {
638            mFilter = null;
639        }
640
641        mPopup.setAdapter(mAdapter);
642    }
643
644    @Override
645    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
646        if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
647                && !mPopup.isDropDownAlwaysVisible()) {
648            // special case for the back key, we do not even try to send it
649            // to the drop down list but instead, consume it immediately
650            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
651                KeyEvent.DispatcherState state = getKeyDispatcherState();
652                if (state != null) {
653                    state.startTracking(event, this);
654                }
655                return true;
656            } else if (event.getAction() == KeyEvent.ACTION_UP) {
657                KeyEvent.DispatcherState state = getKeyDispatcherState();
658                if (state != null) {
659                    state.handleUpEvent(event);
660                }
661                if (event.isTracking() && !event.isCanceled()) {
662                    dismissDropDown();
663                    return true;
664                }
665            }
666        }
667        return super.onKeyPreIme(keyCode, event);
668    }
669
670    @Override
671    public boolean onKeyUp(int keyCode, KeyEvent event) {
672        boolean consumed = mPopup.onKeyUp(keyCode, event);
673        if (consumed) {
674            switch (keyCode) {
675            // if the list accepts the key events and the key event
676            // was a click, the text view gets the selected item
677            // from the drop down as its content
678            case KeyEvent.KEYCODE_ENTER:
679            case KeyEvent.KEYCODE_DPAD_CENTER:
680            case KeyEvent.KEYCODE_TAB:
681                if (event.hasNoModifiers()) {
682                    performCompletion();
683                }
684                return true;
685            }
686        }
687
688        if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
689            performCompletion();
690            return true;
691        }
692
693        return super.onKeyUp(keyCode, event);
694    }
695
696    @Override
697    public boolean onKeyDown(int keyCode, KeyEvent event) {
698        if (mPopup.onKeyDown(keyCode, event)) {
699            return true;
700        }
701
702        if (!isPopupShowing()) {
703            switch(keyCode) {
704            case KeyEvent.KEYCODE_DPAD_DOWN:
705                if (event.hasNoModifiers()) {
706                    performValidation();
707                }
708            }
709        }
710
711        if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
712            return true;
713        }
714
715        mLastKeyCode = keyCode;
716        boolean handled = super.onKeyDown(keyCode, event);
717        mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
718
719        if (handled && isPopupShowing()) {
720            clearListSelection();
721        }
722
723        return handled;
724    }
725
726    /**
727     * Returns <code>true</code> if the amount of text in the field meets
728     * or exceeds the {@link #getThreshold} requirement.  You can override
729     * this to impose a different standard for when filtering will be
730     * triggered.
731     */
732    public boolean enoughToFilter() {
733        if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
734                + " threshold=" + mThreshold);
735        return getText().length() >= mThreshold;
736    }
737
738    /**
739     * This is used to watch for edits to the text view.  Note that we call
740     * to methods on the auto complete text view class so that we can access
741     * private vars without going through thunks.
742     */
743    private class MyWatcher implements TextWatcher {
744        public void afterTextChanged(Editable s) {
745            doAfterTextChanged();
746        }
747        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
748            doBeforeTextChanged();
749        }
750        public void onTextChanged(CharSequence s, int start, int before, int count) {
751        }
752    }
753
754    void doBeforeTextChanged() {
755        if (mBlockCompletion) return;
756
757        // when text is changed, inserted or deleted, we attempt to show
758        // the drop down
759        mOpenBefore = isPopupShowing();
760        if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore);
761    }
762
763    void doAfterTextChanged() {
764        if (mBlockCompletion) return;
765
766        // if the list was open before the keystroke, but closed afterwards,
767        // then something in the keystroke processing (an input filter perhaps)
768        // called performCompletion() and we shouldn't do any more processing.
769        if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore
770                + " open=" + isPopupShowing());
771        if (mOpenBefore && !isPopupShowing()) {
772            return;
773        }
774
775        // the drop down is shown only when a minimum number of characters
776        // was typed in the text view
777        if (enoughToFilter()) {
778            if (mFilter != null) {
779                mPopupCanBeUpdated = true;
780                performFiltering(getText(), mLastKeyCode);
781            }
782        } else {
783            // drop down is automatically dismissed when enough characters
784            // are deleted from the text view
785            if (!mPopup.isDropDownAlwaysVisible()) {
786                dismissDropDown();
787            }
788            if (mFilter != null) {
789                mFilter.filter(null);
790            }
791        }
792    }
793
794    /**
795     * <p>Indicates whether the popup menu is showing.</p>
796     *
797     * @return true if the popup menu is showing, false otherwise
798     */
799    public boolean isPopupShowing() {
800        return mPopup.isShowing();
801    }
802
803    /**
804     * <p>Converts the selected item from the drop down list into a sequence
805     * of character that can be used in the edit box.</p>
806     *
807     * @param selectedItem the item selected by the user for completion
808     *
809     * @return a sequence of characters representing the selected suggestion
810     */
811    protected CharSequence convertSelectionToString(Object selectedItem) {
812        return mFilter.convertResultToString(selectedItem);
813    }
814
815    /**
816     * <p>Clear the list selection.  This may only be temporary, as user input will often bring
817     * it back.
818     */
819    public void clearListSelection() {
820        mPopup.clearListSelection();
821    }
822
823    /**
824     * Set the position of the dropdown view selection.
825     *
826     * @param position The position to move the selector to.
827     */
828    public void setListSelection(int position) {
829        mPopup.setSelection(position);
830    }
831
832    /**
833     * Get the position of the dropdown view selection, if there is one.  Returns
834     * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
835     * there is no selection.
836     *
837     * @return the position of the current selection, if there is one, or
838     * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
839     *
840     * @see ListView#getSelectedItemPosition()
841     */
842    public int getListSelection() {
843        return mPopup.getSelectedItemPosition();
844    }
845
846    /**
847     * <p>Starts filtering the content of the drop down list. The filtering
848     * pattern is the content of the edit box. Subclasses should override this
849     * method to filter with a different pattern, for instance a substring of
850     * <code>text</code>.</p>
851     *
852     * @param text the filtering pattern
853     * @param keyCode the last character inserted in the edit box; beware that
854     * this will be null when text is being added through a soft input method.
855     */
856    @SuppressWarnings({ "UnusedDeclaration" })
857    protected void performFiltering(CharSequence text, int keyCode) {
858        mFilter.filter(text, this);
859    }
860
861    /**
862     * <p>Performs the text completion by converting the selected item from
863     * the drop down list into a string, replacing the text box's content with
864     * this string and finally dismissing the drop down menu.</p>
865     */
866    public void performCompletion() {
867        performCompletion(null, -1, -1);
868    }
869
870    @Override
871    public void onCommitCompletion(CompletionInfo completion) {
872        if (isPopupShowing()) {
873            mPopup.performItemClick(completion.getPosition());
874        }
875    }
876
877    private void performCompletion(View selectedView, int position, long id) {
878        if (isPopupShowing()) {
879            Object selectedItem;
880            if (position < 0) {
881                selectedItem = mPopup.getSelectedItem();
882            } else {
883                selectedItem = mAdapter.getItem(position);
884            }
885            if (selectedItem == null) {
886                Log.w(TAG, "performCompletion: no selected item");
887                return;
888            }
889
890            mBlockCompletion = true;
891            replaceText(convertSelectionToString(selectedItem));
892            mBlockCompletion = false;
893
894            if (mItemClickListener != null) {
895                final ListPopupWindow list = mPopup;
896
897                if (selectedView == null || position < 0) {
898                    selectedView = list.getSelectedView();
899                    position = list.getSelectedItemPosition();
900                    id = list.getSelectedItemId();
901                }
902                mItemClickListener.onItemClick(list.getListView(), selectedView, position, id);
903            }
904        }
905
906        if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) {
907            dismissDropDown();
908        }
909    }
910
911    /**
912     * Identifies whether the view is currently performing a text completion, so subclasses
913     * can decide whether to respond to text changed events.
914     */
915    public boolean isPerformingCompletion() {
916        return mBlockCompletion;
917    }
918
919    /**
920     * Like {@link #setText(CharSequence)}, except that it can disable filtering.
921     *
922     * @param filter If <code>false</code>, no filtering will be performed
923     *        as a result of this call.
924     */
925    public void setText(CharSequence text, boolean filter) {
926        if (filter) {
927            setText(text);
928        } else {
929            mBlockCompletion = true;
930            setText(text);
931            mBlockCompletion = false;
932        }
933    }
934
935    /**
936     * <p>Performs the text completion by replacing the current text by the
937     * selected item. Subclasses should override this method to avoid replacing
938     * the whole content of the edit box.</p>
939     *
940     * @param text the selected suggestion in the drop down list
941     */
942    protected void replaceText(CharSequence text) {
943        clearComposingText();
944
945        setText(text);
946        // make sure we keep the caret at the end of the text view
947        Editable spannable = getText();
948        Selection.setSelection(spannable, spannable.length());
949    }
950
951    /** {@inheritDoc} */
952    public void onFilterComplete(int count) {
953        updateDropDownForFilter(count);
954    }
955
956    private void updateDropDownForFilter(int count) {
957        // Not attached to window, don't update drop-down
958        if (getWindowVisibility() == View.GONE) return;
959
960        /*
961         * This checks enoughToFilter() again because filtering requests
962         * are asynchronous, so the result may come back after enough text
963         * has since been deleted to make it no longer appropriate
964         * to filter.
965         */
966
967        final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible();
968        final boolean enoughToFilter = enoughToFilter();
969        if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter) {
970            if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) {
971                showDropDown();
972            }
973        } else if (!dropDownAlwaysVisible && isPopupShowing()) {
974            dismissDropDown();
975            // When the filter text is changed, the first update from the adapter may show an empty
976            // count (when the query is being performed on the network). Future updates when some
977            // content has been retrieved should still be able to update the list.
978            mPopupCanBeUpdated = true;
979        }
980    }
981
982    @Override
983    public void onWindowFocusChanged(boolean hasWindowFocus) {
984        super.onWindowFocusChanged(hasWindowFocus);
985        if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) {
986            dismissDropDown();
987        }
988    }
989
990    @Override
991    protected void onDisplayHint(int hint) {
992        super.onDisplayHint(hint);
993        switch (hint) {
994            case INVISIBLE:
995                if (!mPopup.isDropDownAlwaysVisible()) {
996                    dismissDropDown();
997                }
998                break;
999        }
1000    }
1001
1002    @Override
1003    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1004        super.onFocusChanged(focused, direction, previouslyFocusedRect);
1005        // Perform validation if the view is losing focus.
1006        if (!focused) {
1007            performValidation();
1008        }
1009        if (!focused && !mPopup.isDropDownAlwaysVisible()) {
1010            dismissDropDown();
1011        }
1012    }
1013
1014    @Override
1015    protected void onAttachedToWindow() {
1016        super.onAttachedToWindow();
1017    }
1018
1019    @Override
1020    protected void onDetachedFromWindow() {
1021        dismissDropDown();
1022        super.onDetachedFromWindow();
1023    }
1024
1025    /**
1026     * <p>Closes the drop down if present on screen.</p>
1027     */
1028    public void dismissDropDown() {
1029        InputMethodManager imm = InputMethodManager.peekInstance();
1030        if (imm != null) {
1031            imm.displayCompletions(this, null);
1032        }
1033        mPopup.dismiss();
1034        mPopupCanBeUpdated = false;
1035    }
1036
1037    @Override
1038    protected boolean setFrame(final int l, int t, final int r, int b) {
1039        boolean result = super.setFrame(l, t, r, b);
1040
1041        if (isPopupShowing()) {
1042            showDropDown();
1043        }
1044
1045        return result;
1046    }
1047
1048    /**
1049     * Issues a runnable to show the dropdown as soon as possible.
1050     *
1051     * @hide internal used only by SearchDialog
1052     */
1053    public void showDropDownAfterLayout() {
1054        mPopup.postShow();
1055    }
1056
1057    /**
1058     * Ensures that the drop down is not obscuring the IME.
1059     * @param visible whether the ime should be in front. If false, the ime is pushed to
1060     * the background.
1061     * @hide internal used only here and SearchDialog
1062     */
1063    public void ensureImeVisible(boolean visible) {
1064        mPopup.setInputMethodMode(visible
1065                ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1066        if (mPopup.isDropDownAlwaysVisible() || (mFilter != null && enoughToFilter())) {
1067            showDropDown();
1068        }
1069    }
1070
1071    /**
1072     * @hide internal used only here and SearchDialog
1073     */
1074    public boolean isInputMethodNotNeeded() {
1075        return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED;
1076    }
1077
1078    /**
1079     * <p>Displays the drop down on screen.</p>
1080     */
1081    public void showDropDown() {
1082        buildImeCompletions();
1083
1084        if (mPopup.getAnchorView() == null) {
1085            if (mDropDownAnchorId != View.NO_ID) {
1086                mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId));
1087            } else {
1088                mPopup.setAnchorView(this);
1089            }
1090        }
1091        if (!isPopupShowing()) {
1092            // Make sure the list does not obscure the IME when shown for the first time.
1093            mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED);
1094            mPopup.setListItemExpandMax(EXPAND_MAX);
1095        }
1096        mPopup.show();
1097        mPopup.getListView().setOverScrollMode(View.OVER_SCROLL_ALWAYS);
1098    }
1099
1100    /**
1101     * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
1102     * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
1103     * ignore outside touch even when the drop down is not set to always visible.
1104     *
1105     * @hide used only by SearchDialog
1106     */
1107    public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
1108        mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch);
1109    }
1110
1111    private void buildImeCompletions() {
1112        final ListAdapter adapter = mAdapter;
1113        if (adapter != null) {
1114            InputMethodManager imm = InputMethodManager.peekInstance();
1115            if (imm != null) {
1116                final int count = Math.min(adapter.getCount(), 20);
1117                CompletionInfo[] completions = new CompletionInfo[count];
1118                int realCount = 0;
1119
1120                for (int i = 0; i < count; i++) {
1121                    if (adapter.isEnabled(i)) {
1122                        Object item = adapter.getItem(i);
1123                        long id = adapter.getItemId(i);
1124                        completions[realCount] = new CompletionInfo(id, realCount,
1125                                convertSelectionToString(item));
1126                        realCount++;
1127                    }
1128                }
1129
1130                if (realCount != count) {
1131                    CompletionInfo[] tmp = new CompletionInfo[realCount];
1132                    System.arraycopy(completions, 0, tmp, 0, realCount);
1133                    completions = tmp;
1134                }
1135
1136                imm.displayCompletions(this, completions);
1137            }
1138        }
1139    }
1140
1141    /**
1142     * Sets the validator used to perform text validation.
1143     *
1144     * @param validator The validator used to validate the text entered in this widget.
1145     *
1146     * @see #getValidator()
1147     * @see #performValidation()
1148     */
1149    public void setValidator(Validator validator) {
1150        mValidator = validator;
1151    }
1152
1153    /**
1154     * Returns the Validator set with {@link #setValidator},
1155     * or <code>null</code> if it was not set.
1156     *
1157     * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
1158     * @see #performValidation()
1159     */
1160    public Validator getValidator() {
1161        return mValidator;
1162    }
1163
1164    /**
1165     * If a validator was set on this view and the current string is not valid,
1166     * ask the validator to fix it.
1167     *
1168     * @see #getValidator()
1169     * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
1170     */
1171    public void performValidation() {
1172        if (mValidator == null) return;
1173
1174        CharSequence text = getText();
1175
1176        if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) {
1177            setText(mValidator.fixText(text));
1178        }
1179    }
1180
1181    /**
1182     * Returns the Filter obtained from {@link Filterable#getFilter},
1183     * or <code>null</code> if {@link #setAdapter} was not called with
1184     * a Filterable.
1185     */
1186    protected Filter getFilter() {
1187        return mFilter;
1188    }
1189
1190    private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
1191        public void onItemClick(AdapterView parent, View v, int position, long id) {
1192            performCompletion(v, position, id);
1193        }
1194    }
1195
1196    /**
1197     * This interface is used to make sure that the text entered in this TextView complies to
1198     * a certain format.  Since there is no foolproof way to prevent the user from leaving
1199     * this View with an incorrect value in it, all we can do is try to fix it ourselves
1200     * when this happens.
1201     */
1202    public interface Validator {
1203        /**
1204         * Validates the specified text.
1205         *
1206         * @return true If the text currently in the text editor is valid.
1207         *
1208         * @see #fixText(CharSequence)
1209         */
1210        boolean isValid(CharSequence text);
1211
1212        /**
1213         * Corrects the specified text to make it valid.
1214         *
1215         * @param invalidText A string that doesn't pass validation: isValid(invalidText)
1216         *        returns false
1217         *
1218         * @return A string based on invalidText such as invoking isValid() on it returns true.
1219         *
1220         * @see #isValid(CharSequence)
1221         */
1222        CharSequence fixText(CharSequence invalidText);
1223    }
1224
1225    /**
1226     * Listener to respond to the AutoCompleteTextView's completion list being dismissed.
1227     * @see AutoCompleteTextView#setOnDismissListener(OnDismissListener)
1228     */
1229    public interface OnDismissListener {
1230        /**
1231         * This method will be invoked whenever the AutoCompleteTextView's list
1232         * of completion options has been dismissed and is no longer available
1233         * for user interaction.
1234         */
1235        void onDismiss();
1236    }
1237
1238    /**
1239     * Allows us a private hook into the on click event without preventing users from setting
1240     * their own click listener.
1241     */
1242    private class PassThroughClickListener implements OnClickListener {
1243
1244        private View.OnClickListener mWrapped;
1245
1246        /** {@inheritDoc} */
1247        public void onClick(View v) {
1248            onClickImpl();
1249
1250            if (mWrapped != null) mWrapped.onClick(v);
1251        }
1252    }
1253
1254    private class PopupDataSetObserver extends DataSetObserver {
1255        @Override
1256        public void onChanged() {
1257            if (mAdapter != null) {
1258                // If the popup is not showing already, showing it will cause
1259                // the list of data set observers attached to the adapter to
1260                // change. We can't do it from here, because we are in the middle
1261                // of iterating through the list of observers.
1262                post(new Runnable() {
1263                    public void run() {
1264                        final ListAdapter adapter = mAdapter;
1265                        if (adapter != null) {
1266                            // This will re-layout, thus resetting mDataChanged, so that the
1267                            // listView click listener stays responsive
1268                            updateDropDownForFilter(adapter.getCount());
1269                        }
1270                    }
1271                });
1272            }
1273        }
1274    }
1275}
1276