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