Home | History | Annotate | Download | only in ui
      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 
     85     public SearchActivityView(Context context) {
     86         super(context);
     87     }
     88 
     89     public SearchActivityView(Context context, AttributeSet attrs) {
     90         super(context, attrs);
     91     }
     92 
     93     public SearchActivityView(Context context, AttributeSet attrs, int defStyle) {
     94         super(context, attrs, defStyle);
     95     }
     96 
     97     @Override
     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 
    134     public abstract void onResume();
    135 
    136     public abstract void onStop();
    137 
    138     public void onPause() {
    139         // Override if necessary
    140     }
    141 
    142     public void start() {
    143         mSuggestionsAdapter.getListAdapter().registerDataSetObserver(new SuggestionsObserver());
    144         mSuggestionsView.setSuggestionsAdapter(mSuggestionsAdapter);
    145     }
    146 
    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.
    153     protected QsbApplication getQsbApplication() {
    154         return QsbApplication.get(getContext());
    155     }
    156 
    157     protected Drawable getVoiceSearchIcon() {
    158         return getResources().getDrawable(R.drawable.ic_btn_speak_now);
    159     }
    160 
    161     protected VoiceSearch getVoiceSearch() {
    162         return getQsbApplication().getVoiceSearch();
    163     }
    164 
    165     protected SuggestionsAdapter<ListAdapter> createSuggestionsAdapter() {
    166         return new DelayingSuggestionsAdapter<ListAdapter>(new SuggestionsListAdapter(
    167                 getQsbApplication().getSuggestionViewFactory()));
    168     }
    169 
    170     protected Corpora getCorpora() {
    171         return getQsbApplication().getCorpora();
    172     }
    173 
    174     public Corpus getCorpus() {
    175         return mCorpus;
    176     }
    177 
    178     protected abstract Promoter createSuggestionsPromoter();
    179 
    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 
    190     public void onCorpusSelected(String corpusName) {
    191         setCorpus(corpusName);
    192         focusQueryTextView();
    193         showInputMethodForQuery();
    194     }
    195 
    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 
    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 
    212     public String getCorpusName() {
    213         Corpus corpus = getCorpus();
    214         return corpus == null ? null : corpus.getName();
    215     }
    216 
    217     public abstract Corpus getSearchCorpus();
    218 
    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 
    227     public void setMaxPromotedSuggestions(int maxPromoted) {
    228         mSuggestionsView.setLimitSuggestionsToViewHeight(false);
    229         mSuggestionsAdapter.setMaxPromoted(maxPromoted);
    230     }
    231 
    232     public void limitSuggestionsToViewHeight() {
    233         mSuggestionsView.setLimitSuggestionsToViewHeight(true);
    234     }
    235 
    236     public void setMaxPromotedResults(int maxPromoted) {
    237     }
    238 
    239     public void limitResultsToViewHeight() {
    240     }
    241 
    242     public void setQueryListener(QueryListener listener) {
    243         mQueryListener = listener;
    244     }
    245 
    246     public void setSearchClickListener(SearchClickListener listener) {
    247         mSearchClickListener = listener;
    248     }
    249 
    250     public abstract void showCorpusSelectionDialog();
    251 
    252     public void setVoiceSearchButtonClickListener(View.OnClickListener listener) {
    253         if (mVoiceSearchButton != null) {
    254             mVoiceSearchButton.setOnClickListener(listener);
    255         }
    256     }
    257 
    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 
    268     public void setExitClickListener(final View.OnClickListener listener) {
    269         mExitClickListener = listener;
    270     }
    271 
    272     public Suggestions getSuggestions() {
    273         return mSuggestionsAdapter.getSuggestions();
    274     }
    275 
    276     public SuggestionCursor getCurrentPromotedSuggestions() {
    277         return mSuggestionsAdapter.getCurrentPromotedSuggestions();
    278     }
    279 
    280     public void setSuggestions(Suggestions suggestions) {
    281         suggestions.acquire();
    282         mSuggestionsAdapter.setSuggestions(suggestions);
    283     }
    284 
    285     public void clearSuggestions() {
    286         mSuggestionsAdapter.setSuggestions(null);
    287     }
    288 
    289     public String getQuery() {
    290         CharSequence q = mQueryTextView.getText();
    291         return q == null ? "" : q.toString();
    292     }
    293 
    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      */
    301     public void setQuery(String query, boolean selectAll) {
    302         mUpdateSuggestions = false;
    303         mQueryTextView.setText(query);
    304         mQueryTextView.setTextSelection(selectAll);
    305         mUpdateSuggestions = true;
    306     }
    307 
    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 
    317     public void hideSuggestions() {
    318         mSuggestionsView.setVisibility(GONE);
    319     }
    320 
    321     public void showSuggestions() {
    322         mSuggestionsView.setVisibility(VISIBLE);
    323     }
    324 
    325     public void focusQueryTextView() {
    326         mQueryTextView.requestFocus();
    327     }
    328 
    329     protected void updateUi() {
    330         updateUi(isQueryEmpty());
    331     }
    332 
    333     protected void updateUi(boolean queryEmpty) {
    334         updateQueryTextView(queryEmpty);
    335         updateSearchGoButton(queryEmpty);
    336         updateVoiceSearchButton(queryEmpty);
    337     }
    338 
    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 
    358     private void updateSearchGoButton(boolean queryEmpty) {
    359         if (queryEmpty) {
    360             mSearchGoButton.setVisibility(View.GONE);
    361         } else {
    362             mSearchGoButton.setVisibility(View.VISIBLE);
    363         }
    364     }
    365 
    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 
    377     protected boolean shouldShowVoiceSearch(boolean queryEmpty) {
    378         return queryEmpty;
    379     }
    380 
    381     /**
    382      * Hides the input method.
    383      */
    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 
    392     public abstract void considerHidingInputMethod();
    393 
    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
    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      */
    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 
    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 
    455     protected void onSuggestionsChanged() {
    456         updateInputMethodSuggestions();
    457     }
    458 
    459     /**
    460      * Checks if the corpus used for typed searches is the web corpus.
    461      */
    462     protected boolean isSearchCorpusWeb() {
    463         Corpus corpus = getSearchCorpus();
    464         return corpus != null && corpus.isWebCorpus();
    465     }
    466 
    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 
    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 {
    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 
    508         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    509         }
    510 
    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 {
    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 
    534         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    535                 int totalItemCount) {
    536         }
    537 
    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 {
    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 {
    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 {
    576         public boolean onKey(View v, int keyCode, KeyEvent event) {
    577             return forwardKeyToQueryTextView(keyCode, event);
    578         }
    579     }
    580 
    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 
    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 {
    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 {
    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
    630         public void onChanged() {
    631             onSuggestionsChanged();
    632         }
    633     }
    634 
    635     public interface QueryListener {
    636         void onQueryChanged();
    637     }
    638 
    639     public interface SearchClickListener {
    640         boolean onSearchClicked(int method);
    641     }
    642 
    643     private class CloseClickListener implements OnClickListener {
    644         public void onClick(View v) {
    645             if (!isQueryEmpty()) {
    646                 mQueryTextView.setText("");
    647             } else {
    648                 mExitClickListener.onClick(v);
    649             }
    650         }
    651     }
    652 }
    653