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