• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.quicksearchbox.ui;
18 
19 import com.android.quicksearchbox.Corpora;
20 import com.android.quicksearchbox.Corpus;
21 import com.android.quicksearchbox.CorpusResult;
22 import com.android.quicksearchbox.Logger;
23 import com.android.quicksearchbox.Promoter;
24 import com.android.quicksearchbox.QsbApplication;
25 import com.android.quicksearchbox.R;
26 import com.android.quicksearchbox.SearchActivity;
27 import com.android.quicksearchbox.SuggestionCursor;
28 import com.android.quicksearchbox.Suggestions;
29 import com.android.quicksearchbox.VoiceSearch;
30 
31 import android.content.Context;
32 import android.database.DataSetObserver;
33 import android.graphics.drawable.Drawable;
34 import android.text.Editable;
35 import android.text.TextUtils;
36 import android.text.TextWatcher;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.view.KeyEvent;
40 import android.view.View;
41 import android.view.inputmethod.CompletionInfo;
42 import android.view.inputmethod.InputMethodManager;
43 import android.widget.AbsListView;
44 import android.widget.ImageButton;
45 import android.widget.ListAdapter;
46 import android.widget.RelativeLayout;
47 import android.widget.TextView;
48 import android.widget.TextView.OnEditorActionListener;
49 
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 
53 public abstract class SearchActivityView extends RelativeLayout {
54     protected static final boolean DBG = false;
55     protected static final String TAG = "QSB.SearchActivityView";
56 
57     // The string used for privateImeOptions to identify to the IME that it should not show
58     // a microphone button since one already exists in the search dialog.
59     // TODO: This should move to android-common or something.
60     private static final String IME_OPTION_NO_MICROPHONE = "nm";
61 
62     private Corpus mCorpus;
63 
64     protected QueryTextView mQueryTextView;
65     // True if the query was empty on the previous call to updateQuery()
66     protected boolean mQueryWasEmpty = true;
67     protected Drawable mQueryTextEmptyBg;
68     protected Drawable mQueryTextNotEmptyBg;
69 
70     protected SuggestionsListView<ListAdapter> mSuggestionsView;
71     protected SuggestionsAdapter<ListAdapter> mSuggestionsAdapter;
72 
73     protected ImageButton mSearchCloseButton;
74     protected ImageButton mSearchGoButton;
75     protected ImageButton mVoiceSearchButton;
76 
77     protected ButtonsKeyListener mButtonsKeyListener;
78 
79     private boolean mUpdateSuggestions;
80 
81     private QueryListener mQueryListener;
82     private SearchClickListener mSearchClickListener;
83     protected View.OnClickListener mExitClickListener;
84 
SearchActivityView(Context context)85     public SearchActivityView(Context context) {
86         super(context);
87     }
88 
SearchActivityView(Context context, AttributeSet attrs)89     public SearchActivityView(Context context, AttributeSet attrs) {
90         super(context, attrs);
91     }
92 
SearchActivityView(Context context, AttributeSet attrs, int defStyle)93     public SearchActivityView(Context context, AttributeSet attrs, int defStyle) {
94         super(context, attrs, defStyle);
95     }
96 
97     @Override
onFinishInflate()98     protected void onFinishInflate() {
99         mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text);
100 
101         mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
102         mSuggestionsView.setOnScrollListener(new InputMethodCloser());
103         mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
104         mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener());
105 
106         mSuggestionsAdapter = createSuggestionsAdapter();
107         // TODO: why do we need focus listeners both on the SuggestionsView and the individual
108         // suggestions?
109         mSuggestionsAdapter.setOnFocusChangeListener(new SuggestListFocusListener());
110 
111         mSearchCloseButton = (ImageButton) findViewById(R.id.search_close_btn);
112         mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
113         mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
114         mVoiceSearchButton.setImageDrawable(getVoiceSearchIcon());
115 
116         mQueryTextView.addTextChangedListener(new SearchTextWatcher());
117         mQueryTextView.setOnEditorActionListener(new QueryTextEditorActionListener());
118         mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
119         mQueryTextEmptyBg = mQueryTextView.getBackground();
120 
121         mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
122 
123         mButtonsKeyListener = new ButtonsKeyListener();
124         mSearchGoButton.setOnKeyListener(mButtonsKeyListener);
125         mVoiceSearchButton.setOnKeyListener(mButtonsKeyListener);
126         if (mSearchCloseButton != null) {
127             mSearchCloseButton.setOnKeyListener(mButtonsKeyListener);
128             mSearchCloseButton.setOnClickListener(new CloseClickListener());
129         }
130 
131         mUpdateSuggestions = true;
132     }
133 
onResume()134     public abstract void onResume();
135 
onStop()136     public abstract void onStop();
137 
onPause()138     public void onPause() {
139         // Override if necessary
140     }
141 
start()142     public void start() {
143         mSuggestionsAdapter.getListAdapter().registerDataSetObserver(new SuggestionsObserver());
144         mSuggestionsView.setSuggestionsAdapter(mSuggestionsAdapter);
145     }
146 
destroy()147     public void destroy() {
148         mSuggestionsView.setSuggestionsAdapter(null);  // closes mSuggestionsAdapter
149     }
150 
151     // TODO: Get rid of this. To make it more easily testable,
152     // the SearchActivityView should not depend on QsbApplication.
getQsbApplication()153     protected QsbApplication getQsbApplication() {
154         return QsbApplication.get(getContext());
155     }
156 
getVoiceSearchIcon()157     protected Drawable getVoiceSearchIcon() {
158         return getResources().getDrawable(R.drawable.ic_btn_speak_now);
159     }
160 
getVoiceSearch()161     protected VoiceSearch getVoiceSearch() {
162         return getQsbApplication().getVoiceSearch();
163     }
164 
createSuggestionsAdapter()165     protected SuggestionsAdapter<ListAdapter> createSuggestionsAdapter() {
166         return new DelayingSuggestionsAdapter<ListAdapter>(new SuggestionsListAdapter(
167                 getQsbApplication().getSuggestionViewFactory()));
168     }
169 
getCorpora()170     protected Corpora getCorpora() {
171         return getQsbApplication().getCorpora();
172     }
173 
getCorpus()174     public Corpus getCorpus() {
175         return mCorpus;
176     }
177 
createSuggestionsPromoter()178     protected abstract Promoter createSuggestionsPromoter();
179 
getCorpus(String sourceName)180     protected Corpus getCorpus(String sourceName) {
181         if (sourceName == null) return null;
182         Corpus corpus = getCorpora().getCorpus(sourceName);
183         if (corpus == null) {
184             Log.w(TAG, "Unknown corpus " + sourceName);
185             return null;
186         }
187         return corpus;
188     }
189 
onCorpusSelected(String corpusName)190     public void onCorpusSelected(String corpusName) {
191         setCorpus(corpusName);
192         focusQueryTextView();
193         showInputMethodForQuery();
194     }
195 
setCorpus(String corpusName)196     public void setCorpus(String corpusName) {
197         if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")");
198         Corpus corpus = getCorpus(corpusName);
199         setCorpus(corpus);
200         updateUi();
201     }
202 
setCorpus(Corpus corpus)203     protected void setCorpus(Corpus corpus) {
204         mCorpus = corpus;
205         mSuggestionsAdapter.setPromoter(createSuggestionsPromoter());
206         Suggestions suggestions = getSuggestions();
207         if (corpus == null || suggestions == null || !suggestions.expectsCorpus(corpus)) {
208             getActivity().updateSuggestions();
209         }
210     }
211 
getCorpusName()212     public String getCorpusName() {
213         Corpus corpus = getCorpus();
214         return corpus == null ? null : corpus.getName();
215     }
216 
getSearchCorpus()217     public abstract Corpus getSearchCorpus();
218 
getWebCorpus()219     public Corpus getWebCorpus() {
220         Corpus webCorpus = getCorpora().getWebCorpus();
221         if (webCorpus == null) {
222             Log.e(TAG, "No web corpus");
223         }
224         return webCorpus;
225     }
226 
setMaxPromotedSuggestions(int maxPromoted)227     public void setMaxPromotedSuggestions(int maxPromoted) {
228         mSuggestionsView.setLimitSuggestionsToViewHeight(false);
229         mSuggestionsAdapter.setMaxPromoted(maxPromoted);
230     }
231 
limitSuggestionsToViewHeight()232     public void limitSuggestionsToViewHeight() {
233         mSuggestionsView.setLimitSuggestionsToViewHeight(true);
234     }
235 
setMaxPromotedResults(int maxPromoted)236     public void setMaxPromotedResults(int maxPromoted) {
237     }
238 
limitResultsToViewHeight()239     public void limitResultsToViewHeight() {
240     }
241 
setQueryListener(QueryListener listener)242     public void setQueryListener(QueryListener listener) {
243         mQueryListener = listener;
244     }
245 
setSearchClickListener(SearchClickListener listener)246     public void setSearchClickListener(SearchClickListener listener) {
247         mSearchClickListener = listener;
248     }
249 
showCorpusSelectionDialog()250     public abstract void showCorpusSelectionDialog();
251 
setVoiceSearchButtonClickListener(View.OnClickListener listener)252     public void setVoiceSearchButtonClickListener(View.OnClickListener listener) {
253         if (mVoiceSearchButton != null) {
254             mVoiceSearchButton.setOnClickListener(listener);
255         }
256     }
257 
setSuggestionClickListener(final SuggestionClickListener listener)258     public void setSuggestionClickListener(final SuggestionClickListener listener) {
259         mSuggestionsAdapter.setSuggestionClickListener(listener);
260         mQueryTextView.setCommitCompletionListener(new QueryTextView.CommitCompletionListener() {
261             @Override
262             public void onCommitCompletion(int position) {
263                 mSuggestionsAdapter.onSuggestionClicked(position);
264             }
265         });
266     }
267 
setExitClickListener(final View.OnClickListener listener)268     public void setExitClickListener(final View.OnClickListener listener) {
269         mExitClickListener = listener;
270     }
271 
getSuggestions()272     public Suggestions getSuggestions() {
273         return mSuggestionsAdapter.getSuggestions();
274     }
275 
getCurrentPromotedSuggestions()276     public SuggestionCursor getCurrentPromotedSuggestions() {
277         return mSuggestionsAdapter.getCurrentPromotedSuggestions();
278     }
279 
setSuggestions(Suggestions suggestions)280     public void setSuggestions(Suggestions suggestions) {
281         suggestions.acquire();
282         mSuggestionsAdapter.setSuggestions(suggestions);
283     }
284 
clearSuggestions()285     public void clearSuggestions() {
286         mSuggestionsAdapter.setSuggestions(null);
287     }
288 
getQuery()289     public String getQuery() {
290         CharSequence q = mQueryTextView.getText();
291         return q == null ? "" : q.toString();
292     }
293 
isQueryEmpty()294     public boolean isQueryEmpty() {
295         return TextUtils.isEmpty(getQuery());
296     }
297 
298     /**
299      * Sets the text in the query box. Does not update the suggestions.
300      */
setQuery(String query, boolean selectAll)301     public void setQuery(String query, boolean selectAll) {
302         mUpdateSuggestions = false;
303         mQueryTextView.setText(query);
304         mQueryTextView.setTextSelection(selectAll);
305         mUpdateSuggestions = true;
306     }
307 
getActivity()308     protected SearchActivity getActivity() {
309         Context context = getContext();
310         if (context instanceof SearchActivity) {
311             return (SearchActivity) context;
312         } else {
313             return null;
314         }
315     }
316 
hideSuggestions()317     public void hideSuggestions() {
318         mSuggestionsView.setVisibility(GONE);
319     }
320 
showSuggestions()321     public void showSuggestions() {
322         mSuggestionsView.setVisibility(VISIBLE);
323     }
324 
focusQueryTextView()325     public void focusQueryTextView() {
326         mQueryTextView.requestFocus();
327     }
328 
updateUi()329     protected void updateUi() {
330         updateUi(isQueryEmpty());
331     }
332 
updateUi(boolean queryEmpty)333     protected void updateUi(boolean queryEmpty) {
334         updateQueryTextView(queryEmpty);
335         updateSearchGoButton(queryEmpty);
336         updateVoiceSearchButton(queryEmpty);
337     }
338 
updateQueryTextView(boolean queryEmpty)339     protected void updateQueryTextView(boolean queryEmpty) {
340         if (queryEmpty) {
341             if (isSearchCorpusWeb()) {
342                 mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg);
343                 mQueryTextView.setHint(null);
344             } else {
345                 if (mQueryTextNotEmptyBg == null) {
346                     mQueryTextNotEmptyBg =
347                             getResources().getDrawable(R.drawable.textfield_search_empty);
348                 }
349                 mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg);
350                 Corpus corpus = getCorpus();
351                 mQueryTextView.setHint(corpus == null ? "" : corpus.getHint());
352             }
353         } else {
354             mQueryTextView.setBackgroundResource(R.drawable.textfield_search);
355         }
356     }
357 
updateSearchGoButton(boolean queryEmpty)358     private void updateSearchGoButton(boolean queryEmpty) {
359         if (queryEmpty) {
360             mSearchGoButton.setVisibility(View.GONE);
361         } else {
362             mSearchGoButton.setVisibility(View.VISIBLE);
363         }
364     }
365 
updateVoiceSearchButton(boolean queryEmpty)366     protected void updateVoiceSearchButton(boolean queryEmpty) {
367         if (shouldShowVoiceSearch(queryEmpty)
368                 && getVoiceSearch().shouldShowVoiceSearch(getCorpus())) {
369             mVoiceSearchButton.setVisibility(View.VISIBLE);
370             mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
371         } else {
372             mVoiceSearchButton.setVisibility(View.GONE);
373             mQueryTextView.setPrivateImeOptions(null);
374         }
375     }
376 
shouldShowVoiceSearch(boolean queryEmpty)377     protected boolean shouldShowVoiceSearch(boolean queryEmpty) {
378         return queryEmpty;
379     }
380 
381     /**
382      * Hides the input method.
383      */
hideInputMethod()384     protected void hideInputMethod() {
385         InputMethodManager imm = (InputMethodManager)
386                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
387         if (imm != null) {
388             imm.hideSoftInputFromWindow(getWindowToken(), 0);
389         }
390     }
391 
considerHidingInputMethod()392     public abstract void considerHidingInputMethod();
393 
showInputMethodForQuery()394     public void showInputMethodForQuery() {
395         mQueryTextView.showInputMethod();
396     }
397 
398     /**
399      * Dismiss the activity if BACK is pressed when the search box is empty.
400      */
401     @Override
dispatchKeyEventPreIme(KeyEvent event)402     public boolean dispatchKeyEventPreIme(KeyEvent event) {
403         SearchActivity activity = getActivity();
404         if (activity != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK
405                 && isQueryEmpty()) {
406             KeyEvent.DispatcherState state = getKeyDispatcherState();
407             if (state != null) {
408                 if (event.getAction() == KeyEvent.ACTION_DOWN
409                         && event.getRepeatCount() == 0) {
410                     state.startTracking(event, this);
411                     return true;
412                 } else if (event.getAction() == KeyEvent.ACTION_UP
413                         && !event.isCanceled() && state.isTracking(event)) {
414                     hideInputMethod();
415                     activity.onBackPressed();
416                     return true;
417                 }
418             }
419         }
420         return super.dispatchKeyEventPreIme(event);
421     }
422 
423     /**
424      * If the input method is in fullscreen mode, and the selector corpus
425      * is All or Web, use the web search suggestions as completions.
426      */
updateInputMethodSuggestions()427     protected void updateInputMethodSuggestions() {
428         InputMethodManager imm = (InputMethodManager)
429                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
430         if (imm == null || !imm.isFullscreenMode()) return;
431         Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
432         if (suggestions == null) return;
433         CompletionInfo[] completions = webSuggestionsToCompletions(suggestions);
434         if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
435         imm.displayCompletions(mQueryTextView, completions);
436     }
437 
webSuggestionsToCompletions(Suggestions suggestions)438     private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) {
439         // TODO: This should also include include web search shortcuts
440         CorpusResult cursor = suggestions.getWebResult();
441         if (cursor == null) return null;
442         int count = cursor.getCount();
443         ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
444         boolean usingWebCorpus = isSearchCorpusWeb();
445         for (int i = 0; i < count; i++) {
446             cursor.moveTo(i);
447             if (!usingWebCorpus || cursor.isWebSearchSuggestion()) {
448                 String text1 = cursor.getSuggestionText1();
449                 completions.add(new CompletionInfo(i, i, text1));
450             }
451         }
452         return completions.toArray(new CompletionInfo[completions.size()]);
453     }
454 
onSuggestionsChanged()455     protected void onSuggestionsChanged() {
456         updateInputMethodSuggestions();
457     }
458 
459     /**
460      * Checks if the corpus used for typed searches is the web corpus.
461      */
isSearchCorpusWeb()462     protected boolean isSearchCorpusWeb() {
463         Corpus corpus = getSearchCorpus();
464         return corpus != null && corpus.isWebCorpus();
465     }
466 
onSuggestionKeyDown(SuggestionsAdapter<?> adapter, long suggestionId, int keyCode, KeyEvent event)467     protected boolean onSuggestionKeyDown(SuggestionsAdapter<?> adapter,
468             long suggestionId, int keyCode, KeyEvent event) {
469         // Treat enter or search as a click
470         if (       keyCode == KeyEvent.KEYCODE_ENTER
471                 || keyCode == KeyEvent.KEYCODE_SEARCH
472                 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
473             if (adapter != null) {
474                 adapter.onSuggestionClicked(suggestionId);
475                 return true;
476             } else {
477                 return false;
478             }
479         }
480 
481         return false;
482     }
483 
onSearchClicked(int method)484     protected boolean onSearchClicked(int method) {
485         if (mSearchClickListener != null) {
486             return mSearchClickListener.onSearchClicked(method);
487         }
488         return false;
489     }
490 
491     /**
492      * Filters the suggestions list when the search text changes.
493      */
494     private class SearchTextWatcher implements TextWatcher {
afterTextChanged(Editable s)495         public void afterTextChanged(Editable s) {
496             boolean empty = s.length() == 0;
497             if (empty != mQueryWasEmpty) {
498                 mQueryWasEmpty = empty;
499                 updateUi(empty);
500             }
501             if (mUpdateSuggestions) {
502                 if (mQueryListener != null) {
503                     mQueryListener.onQueryChanged();
504                 }
505             }
506         }
507 
beforeTextChanged(CharSequence s, int start, int count, int after)508         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
509         }
510 
onTextChanged(CharSequence s, int start, int before, int count)511         public void onTextChanged(CharSequence s, int start, int before, int count) {
512         }
513     }
514 
515     /**
516      * Handles key events on the suggestions list view.
517      */
518     protected class SuggestionsViewKeyListener implements View.OnKeyListener {
onKey(View v, int keyCode, KeyEvent event)519         public boolean onKey(View v, int keyCode, KeyEvent event) {
520             if (event.getAction() == KeyEvent.ACTION_DOWN
521                     && v instanceof SuggestionsListView<?>) {
522                 SuggestionsListView<?> listView = (SuggestionsListView<?>) v;
523                 if (onSuggestionKeyDown(listView.getSuggestionsAdapter(),
524                         listView.getSelectedItemId(), keyCode, event)) {
525                     return true;
526                 }
527             }
528             return forwardKeyToQueryTextView(keyCode, event);
529         }
530     }
531 
532     private class InputMethodCloser implements SuggestionsView.OnScrollListener {
533 
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)534         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
535                 int totalItemCount) {
536         }
537 
onScrollStateChanged(AbsListView view, int scrollState)538         public void onScrollStateChanged(AbsListView view, int scrollState) {
539             considerHidingInputMethod();
540         }
541     }
542 
543     /**
544      * Listens for clicks on the source selector.
545      */
546     private class SearchGoButtonClickListener implements View.OnClickListener {
onClick(View view)547         public void onClick(View view) {
548             onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
549         }
550     }
551 
552     /**
553      * This class handles enter key presses in the query text view.
554      */
555     private class QueryTextEditorActionListener implements OnEditorActionListener {
onEditorAction(TextView v, int actionId, KeyEvent event)556         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
557             boolean consumed = false;
558             if (event != null) {
559                 if (event.getAction() == KeyEvent.ACTION_UP) {
560                     consumed = onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
561                 } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
562                     // we have to consume the down event so that we receive the up event too
563                     consumed = true;
564                 }
565             }
566             if (DBG) Log.d(TAG, "onEditorAction consumed=" + consumed);
567             return consumed;
568         }
569     }
570 
571     /**
572      * Handles key events on the search and voice search buttons,
573      * by refocusing to EditText.
574      */
575     private class ButtonsKeyListener implements View.OnKeyListener {
onKey(View v, int keyCode, KeyEvent event)576         public boolean onKey(View v, int keyCode, KeyEvent event) {
577             return forwardKeyToQueryTextView(keyCode, event);
578         }
579     }
580 
forwardKeyToQueryTextView(int keyCode, KeyEvent event)581     private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
582         if (!event.isSystem() && shouldForwardToQueryTextView(keyCode)) {
583             if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
584             if (mQueryTextView.requestFocus()) {
585                 return mQueryTextView.dispatchKeyEvent(event);
586             }
587         }
588         return false;
589     }
590 
shouldForwardToQueryTextView(int keyCode)591     private boolean shouldForwardToQueryTextView(int keyCode) {
592         switch (keyCode) {
593             case KeyEvent.KEYCODE_DPAD_UP:
594             case KeyEvent.KEYCODE_DPAD_DOWN:
595             case KeyEvent.KEYCODE_DPAD_LEFT:
596             case KeyEvent.KEYCODE_DPAD_RIGHT:
597             case KeyEvent.KEYCODE_DPAD_CENTER:
598             case KeyEvent.KEYCODE_ENTER:
599             case KeyEvent.KEYCODE_SEARCH:
600                 return false;
601             default:
602                 return true;
603         }
604     }
605 
606     /**
607      * Hides the input method when the suggestions get focus.
608      */
609     private class SuggestListFocusListener implements OnFocusChangeListener {
onFocusChange(View v, boolean focused)610         public void onFocusChange(View v, boolean focused) {
611             if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
612             if (focused) {
613                 considerHidingInputMethod();
614             }
615         }
616     }
617 
618     private class QueryTextViewFocusListener implements OnFocusChangeListener {
onFocusChange(View v, boolean focused)619         public void onFocusChange(View v, boolean focused) {
620             if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
621             if (focused) {
622                 // The query box got focus, show the input method
623                 showInputMethodForQuery();
624             }
625         }
626     }
627 
628     protected class SuggestionsObserver extends DataSetObserver {
629         @Override
onChanged()630         public void onChanged() {
631             onSuggestionsChanged();
632         }
633     }
634 
635     public interface QueryListener {
onQueryChanged()636         void onQueryChanged();
637     }
638 
639     public interface SearchClickListener {
onSearchClicked(int method)640         boolean onSearchClicked(int method);
641     }
642 
643     private class CloseClickListener implements OnClickListener {
onClick(View v)644         public void onClick(View v) {
645             if (!isQueryEmpty()) {
646                 mQueryTextView.setText("");
647             } else {
648                 mExitClickListener.onClick(v);
649             }
650         }
651     }
652 }
653