Home | History | Annotate | Download | only in quicksearchbox
      1 /*
      2  * Copyright (C) 2009 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;
     18 
     19 import com.android.common.Search;
     20 import com.android.quicksearchbox.ui.CorpusViewFactory;
     21 import com.android.quicksearchbox.ui.QueryTextView;
     22 import com.android.quicksearchbox.ui.SuggestionClickListener;
     23 import com.android.quicksearchbox.ui.SuggestionsAdapter;
     24 import com.android.quicksearchbox.ui.SuggestionsView;
     25 import com.google.common.base.CharMatcher;
     26 
     27 import android.app.Activity;
     28 import android.app.SearchManager;
     29 import android.content.DialogInterface;
     30 import android.content.Intent;
     31 import android.database.DataSetObserver;
     32 import android.graphics.drawable.Drawable;
     33 import android.net.Uri;
     34 import android.os.Bundle;
     35 import android.os.Debug;
     36 import android.os.Handler;
     37 import android.text.Editable;
     38 import android.text.TextUtils;
     39 import android.text.TextWatcher;
     40 import android.util.Log;
     41 import android.view.KeyEvent;
     42 import android.view.Menu;
     43 import android.view.View;
     44 import android.view.View.OnFocusChangeListener;
     45 import android.view.inputmethod.CompletionInfo;
     46 import android.view.inputmethod.InputMethodManager;
     47 import android.widget.AbsListView;
     48 import android.widget.ImageButton;
     49 
     50 import java.io.File;
     51 import java.util.ArrayList;
     52 import java.util.Arrays;
     53 import java.util.Set;
     54 
     55 /**
     56  * The main activity for Quick Search Box. Shows the search UI.
     57  *
     58  */
     59 public class SearchActivity extends Activity {
     60 
     61     private static final boolean DBG = false;
     62     private static final String TAG = "QSB.SearchActivity";
     63     private static final boolean TRACE = false;
     64 
     65     private static final String SCHEME_CORPUS = "qsb.corpus";
     66 
     67     public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS
     68             = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS";
     69 
     70     // The string used for privateImeOptions to identify to the IME that it should not show
     71     // a microphone button since one already exists in the search dialog.
     72     // TODO: This should move to android-common or something.
     73     private static final String IME_OPTION_NO_MICROPHONE = "nm";
     74 
     75     // Keys for the saved instance state.
     76     private static final String INSTANCE_KEY_CORPUS = "corpus";
     77     private static final String INSTANCE_KEY_QUERY = "query";
     78 
     79     // Measures time from for last onCreate()/onNewIntent() call.
     80     private LatencyTracker mStartLatencyTracker;
     81     // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume().
     82     private boolean mStarting;
     83     // True if the user has taken some action, e.g. launching a search, voice search,
     84     // or suggestions, since QSB was last started.
     85     private boolean mTookAction;
     86 
     87     private CorpusSelectionDialog mCorpusSelectionDialog;
     88 
     89     protected SuggestionsAdapter mSuggestionsAdapter;
     90 
     91     private CorporaObserver mCorporaObserver;
     92 
     93     protected QueryTextView mQueryTextView;
     94     // True if the query was empty on the previous call to updateQuery()
     95     protected boolean mQueryWasEmpty = true;
     96     protected Drawable mQueryTextEmptyBg;
     97     protected Drawable mQueryTextNotEmptyBg;
     98 
     99     protected SuggestionsView mSuggestionsView;
    100 
    101     protected ImageButton mSearchGoButton;
    102     protected ImageButton mVoiceSearchButton;
    103     protected ImageButton mCorpusIndicator;
    104 
    105     private Corpus mCorpus;
    106     private Bundle mAppSearchData;
    107     private boolean mUpdateSuggestions;
    108 
    109     private final Handler mHandler = new Handler();
    110     private final Runnable mUpdateSuggestionsTask = new Runnable() {
    111         public void run() {
    112             updateSuggestions(getQuery());
    113         }
    114     };
    115 
    116     private final Runnable mShowInputMethodTask = new Runnable() {
    117         public void run() {
    118             showInputMethodForQuery();
    119         }
    120     };
    121 
    122     /** Called when the activity is first created. */
    123     @Override
    124     public void onCreate(Bundle savedInstanceState) {
    125         if (TRACE) startMethodTracing();
    126         recordStartTime();
    127         if (DBG) Log.d(TAG, "onCreate()");
    128         super.onCreate(savedInstanceState);
    129 
    130         setContentView();
    131         SuggestListFocusListener suggestionFocusListener = new SuggestListFocusListener();
    132         mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter();
    133         mSuggestionsAdapter.setSuggestionClickListener(new ClickHandler());
    134         mSuggestionsAdapter.setOnFocusChangeListener(suggestionFocusListener);
    135 
    136         mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text);
    137         mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
    138         mSuggestionsView.setOnScrollListener(new InputMethodCloser());
    139         mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
    140         mSuggestionsView.setOnFocusChangeListener(suggestionFocusListener);
    141 
    142         mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
    143         mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
    144         mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator);
    145 
    146         mQueryTextView.addTextChangedListener(new SearchTextWatcher());
    147         mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
    148         mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
    149         mQueryTextView.setSuggestionClickListener(new ClickHandler());
    150         mQueryTextEmptyBg = mQueryTextView.getBackground();
    151 
    152         mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener());
    153 
    154         mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
    155 
    156         mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener());
    157 
    158         ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener();
    159         mSearchGoButton.setOnKeyListener(buttonsKeyListener);
    160         mVoiceSearchButton.setOnKeyListener(buttonsKeyListener);
    161         mCorpusIndicator.setOnKeyListener(buttonsKeyListener);
    162 
    163         mUpdateSuggestions = true;
    164 
    165         // First get setup from intent
    166         Intent intent = getIntent();
    167         setupFromIntent(intent);
    168         // Then restore any saved instance state
    169         restoreInstanceState(savedInstanceState);
    170 
    171         mSuggestionsAdapter.registerDataSetObserver(new SuggestionsObserver());
    172 
    173         // Do this at the end, to avoid updating the list view when setSource()
    174         // is called.
    175         mSuggestionsView.setAdapter(mSuggestionsAdapter);
    176 
    177         mCorporaObserver = new CorporaObserver();
    178         getCorpora().registerDataSetObserver(mCorporaObserver);
    179     }
    180 
    181     protected void setContentView() {
    182         setContentView(R.layout.search_activity);
    183     }
    184 
    185     private void startMethodTracing() {
    186         File traceDir = getDir("traces", 0);
    187         String traceFile = new File(traceDir, "qsb.trace").getAbsolutePath();
    188         Debug.startMethodTracing(traceFile);
    189     }
    190 
    191     @Override
    192     protected void onNewIntent(Intent intent) {
    193         if (DBG) Log.d(TAG, "onNewIntent()");
    194         recordStartTime();
    195         setIntent(intent);
    196         setupFromIntent(intent);
    197     }
    198 
    199     private void recordStartTime() {
    200         mStartLatencyTracker = new LatencyTracker();
    201         mStarting = true;
    202         mTookAction = false;
    203     }
    204 
    205     protected void restoreInstanceState(Bundle savedInstanceState) {
    206         if (savedInstanceState == null) return;
    207         String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS);
    208         String query = savedInstanceState.getString(INSTANCE_KEY_QUERY);
    209         setCorpus(corpusName);
    210         setQuery(query, false);
    211     }
    212 
    213     @Override
    214     protected void onSaveInstanceState(Bundle outState) {
    215         super.onSaveInstanceState(outState);
    216         // We don't save appSearchData, since we always get the value
    217         // from the intent and the user can't change it.
    218 
    219         outState.putString(INSTANCE_KEY_CORPUS, getCorpusName());
    220         outState.putString(INSTANCE_KEY_QUERY, getQuery());
    221     }
    222 
    223     private void setupFromIntent(Intent intent) {
    224         if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
    225         String corpusName = getCorpusNameFromUri(intent.getData());
    226         String query = intent.getStringExtra(SearchManager.QUERY);
    227         Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
    228         boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
    229 
    230         setCorpus(corpusName);
    231         setQuery(query, selectAll);
    232         mAppSearchData = appSearchData;
    233 
    234         if (startedIntoCorpusSelectionDialog()) {
    235             showCorpusSelectionDialog();
    236         }
    237     }
    238 
    239     public boolean startedIntoCorpusSelectionDialog() {
    240         return INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(getIntent().getAction());
    241     }
    242 
    243     /**
    244      * Removes corpus selector intent action, so that BACK works normally after
    245      * dismissing and reopening the corpus selector.
    246      */
    247     private void clearStartedIntoCorpusSelectionDialog() {
    248         Intent oldIntent = getIntent();
    249         if (SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(oldIntent.getAction())) {
    250             Intent newIntent = new Intent(oldIntent);
    251             newIntent.setAction(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
    252             setIntent(newIntent);
    253         }
    254     }
    255 
    256     public static Uri getCorpusUri(Corpus corpus) {
    257         if (corpus == null) return null;
    258         return new Uri.Builder()
    259                 .scheme(SCHEME_CORPUS)
    260                 .authority(corpus.getName())
    261                 .build();
    262     }
    263 
    264     private String getCorpusNameFromUri(Uri uri) {
    265         if (uri == null) return null;
    266         if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
    267         return uri.getAuthority();
    268     }
    269 
    270     private Corpus getCorpus(String sourceName) {
    271         if (sourceName == null) return null;
    272         Corpus corpus = getCorpora().getCorpus(sourceName);
    273         if (corpus == null) {
    274             Log.w(TAG, "Unknown corpus " + sourceName);
    275             return null;
    276         }
    277         return corpus;
    278     }
    279 
    280     private void setCorpus(String corpusName) {
    281         if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")");
    282         mCorpus = getCorpus(corpusName);
    283         Drawable sourceIcon;
    284         if (mCorpus == null) {
    285             sourceIcon = getCorpusViewFactory().getGlobalSearchIcon();
    286         } else {
    287             sourceIcon = mCorpus.getCorpusIcon();
    288         }
    289         mSuggestionsAdapter.setCorpus(mCorpus);
    290         mCorpusIndicator.setImageDrawable(sourceIcon);
    291 
    292         updateUi(getQuery().length() == 0);
    293     }
    294 
    295     private String getCorpusName() {
    296         return mCorpus == null ? null : mCorpus.getName();
    297     }
    298 
    299     private QsbApplication getQsbApplication() {
    300         return QsbApplication.get(this);
    301     }
    302 
    303     private Config getConfig() {
    304         return getQsbApplication().getConfig();
    305     }
    306 
    307     private Corpora getCorpora() {
    308         return getQsbApplication().getCorpora();
    309     }
    310 
    311     private ShortcutRepository getShortcutRepository() {
    312         return getQsbApplication().getShortcutRepository();
    313     }
    314 
    315     private SuggestionsProvider getSuggestionsProvider() {
    316         return getQsbApplication().getSuggestionsProvider();
    317     }
    318 
    319     private CorpusViewFactory getCorpusViewFactory() {
    320         return getQsbApplication().getCorpusViewFactory();
    321     }
    322 
    323     private VoiceSearch getVoiceSearch() {
    324         return QsbApplication.get(this).getVoiceSearch();
    325     }
    326 
    327     private Logger getLogger() {
    328         return getQsbApplication().getLogger();
    329     }
    330 
    331     @Override
    332     protected void onDestroy() {
    333         if (DBG) Log.d(TAG, "onDestroy()");
    334         super.onDestroy();
    335         getCorpora().unregisterDataSetObserver(mCorporaObserver);
    336         mSuggestionsView.setAdapter(null);  // closes mSuggestionsAdapter
    337     }
    338 
    339     @Override
    340     protected void onStop() {
    341         if (DBG) Log.d(TAG, "onStop()");
    342         if (!mTookAction) {
    343             // TODO: This gets logged when starting other activities, e.g. by opening he search
    344             // settings, or clicking a notification in the status bar.
    345             getLogger().logExit(getCurrentSuggestions(), getQuery().length());
    346         }
    347         // Close all open suggestion cursors. The query will be redone in onResume()
    348         // if we come back to this activity.
    349         mSuggestionsAdapter.setSuggestions(null);
    350         getQsbApplication().getShortcutRefresher().reset();
    351         dismissCorpusSelectionDialog();
    352         super.onStop();
    353     }
    354 
    355     @Override
    356     protected void onRestart() {
    357         if (DBG) Log.d(TAG, "onRestart()");
    358         super.onRestart();
    359     }
    360 
    361     @Override
    362     protected void onResume() {
    363         if (DBG) Log.d(TAG, "onResume()");
    364         super.onResume();
    365         updateSuggestionsBuffered();
    366         if (!isCorpusSelectionDialogShowing()) {
    367             mQueryTextView.requestFocus();
    368         }
    369         if (TRACE) Debug.stopMethodTracing();
    370     }
    371 
    372     @Override
    373     public boolean onCreateOptionsMenu(Menu menu) {
    374         super.onCreateOptionsMenu(menu);
    375         SearchSettings.addSearchSettingsMenuItem(this, menu);
    376         return true;
    377     }
    378 
    379     @Override
    380     public void onWindowFocusChanged(boolean hasFocus) {
    381         super.onWindowFocusChanged(hasFocus);
    382         if (hasFocus) {
    383             // Launch the IME after a bit
    384             mHandler.postDelayed(mShowInputMethodTask, 0);
    385         }
    386     }
    387 
    388     protected String getQuery() {
    389         CharSequence q = mQueryTextView.getText();
    390         return q == null ? "" : q.toString();
    391     }
    392 
    393     /**
    394      * Sets the text in the query box. Does not update the suggestions.
    395      */
    396     private void setQuery(String query, boolean selectAll) {
    397         mUpdateSuggestions = false;
    398         mQueryTextView.setText(query);
    399         mQueryTextView.setTextSelection(selectAll);
    400         mUpdateSuggestions = true;
    401     }
    402 
    403     protected void updateUi(boolean queryEmpty) {
    404         updateQueryTextView(queryEmpty);
    405         updateSearchGoButton(queryEmpty);
    406         updateVoiceSearchButton(queryEmpty);
    407     }
    408 
    409     private void updateQueryTextView(boolean queryEmpty) {
    410         if (queryEmpty) {
    411             if (isSearchCorpusWeb()) {
    412                 mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg);
    413                 mQueryTextView.setHint(null);
    414             } else {
    415                 if (mQueryTextNotEmptyBg == null) {
    416                     mQueryTextNotEmptyBg =
    417                             getResources().getDrawable(R.drawable.textfield_search_empty);
    418                 }
    419                 mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg);
    420                 mQueryTextView.setHint(mCorpus.getHint());
    421             }
    422         } else {
    423             mQueryTextView.setBackgroundResource(R.drawable.textfield_search);
    424         }
    425     }
    426 
    427     private void updateSearchGoButton(boolean queryEmpty) {
    428         if (queryEmpty) {
    429             mSearchGoButton.setVisibility(View.GONE);
    430         } else {
    431             mSearchGoButton.setVisibility(View.VISIBLE);
    432         }
    433     }
    434 
    435     protected void updateVoiceSearchButton(boolean queryEmpty) {
    436         if (queryEmpty && getVoiceSearch().shouldShowVoiceSearch(mCorpus)) {
    437             mVoiceSearchButton.setVisibility(View.VISIBLE);
    438             mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
    439         } else {
    440             mVoiceSearchButton.setVisibility(View.GONE);
    441             mQueryTextView.setPrivateImeOptions(null);
    442         }
    443     }
    444 
    445     protected void showCorpusSelectionDialog() {
    446         if (mCorpusSelectionDialog == null) {
    447             mCorpusSelectionDialog = createCorpusSelectionDialog();
    448             mCorpusSelectionDialog.setOwnerActivity(this);
    449             mCorpusSelectionDialog.setOnDismissListener(new CorpusSelectorDismissListener());
    450             mCorpusSelectionDialog.setOnCorpusSelectedListener(new CorpusSelectionListener());
    451         }
    452         mCorpusSelectionDialog.show(mCorpus);
    453     }
    454 
    455     protected CorpusSelectionDialog createCorpusSelectionDialog() {
    456         return new CorpusSelectionDialog(this);
    457     }
    458 
    459     protected boolean isCorpusSelectionDialogShowing() {
    460         return mCorpusSelectionDialog != null && mCorpusSelectionDialog.isShowing();
    461     }
    462 
    463     protected void dismissCorpusSelectionDialog() {
    464         if (mCorpusSelectionDialog != null) {
    465             mCorpusSelectionDialog.dismiss();
    466         }
    467     }
    468 
    469     /**
    470      * @return true if a search was performed as a result of this click, false otherwise.
    471      */
    472     protected boolean onSearchClicked(int method) {
    473         String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' ');
    474         if (DBG) Log.d(TAG, "Search clicked, query=" + query);
    475 
    476         // Don't do empty queries
    477         if (TextUtils.getTrimmedLength(query) == 0) return false;
    478 
    479         Corpus searchCorpus = getSearchCorpus();
    480         if (searchCorpus == null) return false;
    481 
    482         mTookAction = true;
    483 
    484         // Log search start
    485         getLogger().logSearch(mCorpus, method, query.length());
    486 
    487         // Create shortcut
    488         SuggestionData searchShortcut = searchCorpus.createSearchShortcut(query);
    489         if (searchShortcut != null) {
    490             ListSuggestionCursor cursor = new ListSuggestionCursor(query);
    491             cursor.add(searchShortcut);
    492             getShortcutRepository().reportClick(cursor, 0);
    493         }
    494 
    495         // Start search
    496         Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
    497         launchIntent(intent);
    498         return true;
    499     }
    500 
    501     protected void onVoiceSearchClicked() {
    502         if (DBG) Log.d(TAG, "Voice Search clicked");
    503         Corpus searchCorpus = getSearchCorpus();
    504         if (searchCorpus == null) return;
    505 
    506         mTookAction = true;
    507 
    508         // Log voice search start
    509         getLogger().logVoiceSearch(searchCorpus);
    510 
    511         // Start voice search
    512         Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
    513         launchIntent(intent);
    514     }
    515 
    516     /**
    517      * Gets the corpus to use for any searches. This is the web corpus in "All" mode,
    518      * and the selected corpus otherwise.
    519      */
    520     protected Corpus getSearchCorpus() {
    521         if (mCorpus != null) {
    522             return mCorpus;
    523         } else {
    524             Corpus webCorpus = getCorpora().getWebCorpus();
    525             if (webCorpus == null) {
    526                 Log.e(TAG, "No web corpus");
    527             }
    528             return webCorpus;
    529         }
    530     }
    531 
    532     /**
    533      * Checks if the corpus used for typed searchs is the web corpus.
    534      */
    535     protected boolean isSearchCorpusWeb() {
    536         Corpus corpus = getSearchCorpus();
    537         return corpus != null && corpus.isWebCorpus();
    538     }
    539 
    540     protected SuggestionCursor getCurrentSuggestions() {
    541         return mSuggestionsAdapter.getCurrentSuggestions();
    542     }
    543 
    544     protected SuggestionCursor getCurrentSuggestions(int position) {
    545         SuggestionCursor suggestions = getCurrentSuggestions();
    546         if (suggestions == null) {
    547             return null;
    548         }
    549         int count = suggestions.getCount();
    550         if (position < 0 || position >= count) {
    551             Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count);
    552             return null;
    553         }
    554         suggestions.moveTo(position);
    555         return suggestions;
    556     }
    557 
    558     protected Set<Corpus> getCurrentIncludedCorpora() {
    559         Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
    560         return suggestions == null ? null : suggestions.getIncludedCorpora();
    561     }
    562 
    563     protected void launchIntent(Intent intent) {
    564         if (intent == null) {
    565             return;
    566         }
    567         try {
    568             startActivity(intent);
    569         } catch (RuntimeException ex) {
    570             // Since the intents for suggestions specified by suggestion providers,
    571             // guard against them not being handled, not allowed, etc.
    572             Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
    573         }
    574     }
    575 
    576     protected boolean launchSuggestion(int position) {
    577         SuggestionCursor suggestions = getCurrentSuggestions(position);
    578         if (suggestions == null) return false;
    579 
    580         if (DBG) Log.d(TAG, "Launching suggestion " + position);
    581         mTookAction = true;
    582 
    583         // Log suggestion click
    584         getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
    585                 Logger.SUGGESTION_CLICK_TYPE_LAUNCH);
    586 
    587         // Create shortcut
    588         getShortcutRepository().reportClick(suggestions, position);
    589 
    590         // Launch intent
    591         suggestions.moveTo(position);
    592         Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData);
    593         launchIntent(intent);
    594 
    595         return true;
    596     }
    597 
    598     protected void clickedQuickContact(int position) {
    599         SuggestionCursor suggestions = getCurrentSuggestions(position);
    600         if (suggestions == null) return;
    601 
    602         if (DBG) Log.d(TAG, "Used suggestion " + position);
    603         mTookAction = true;
    604 
    605         // Log suggestion click
    606         getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
    607                 Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT);
    608 
    609         // Create shortcut
    610         getShortcutRepository().reportClick(suggestions, position);
    611     }
    612 
    613     protected boolean onSuggestionLongClicked(int position) {
    614         if (DBG) Log.d(TAG, "Long clicked on suggestion " + position);
    615         return false;
    616     }
    617 
    618     protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) {
    619         // Treat enter or search as a click
    620         if (       keyCode == KeyEvent.KEYCODE_ENTER
    621                 || keyCode == KeyEvent.KEYCODE_SEARCH
    622                 || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
    623             return launchSuggestion(position);
    624         }
    625 
    626         return false;
    627     }
    628 
    629     protected void refineSuggestion(int position) {
    630         if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
    631         SuggestionCursor suggestions = getCurrentSuggestions(position);
    632         if (suggestions == null) {
    633             return;
    634         }
    635         String query = suggestions.getSuggestionQuery();
    636         if (TextUtils.isEmpty(query)) {
    637             return;
    638         }
    639 
    640         // Log refine click
    641         getLogger().logSuggestionClick(position, suggestions, getCurrentIncludedCorpora(),
    642                 Logger.SUGGESTION_CLICK_TYPE_REFINE);
    643 
    644         // Put query + space in query text view
    645         String queryWithSpace = query + ' ';
    646         setQuery(queryWithSpace, false);
    647         updateSuggestions(queryWithSpace);
    648         mQueryTextView.requestFocus();
    649     }
    650 
    651     protected int getSelectedPosition() {
    652         return mSuggestionsView.getSelectedPosition();
    653     }
    654 
    655     /**
    656      * Hides the input method.
    657      */
    658     protected void hideInputMethod() {
    659         mQueryTextView.hideInputMethod();
    660     }
    661 
    662     protected void showInputMethodForQuery() {
    663         mQueryTextView.showInputMethod();
    664     }
    665 
    666     protected void onSuggestionListFocusChange(boolean focused) {
    667     }
    668 
    669     protected void onQueryTextViewFocusChange(boolean focused) {
    670     }
    671 
    672     /**
    673      * Hides the input method when the suggestions get focus.
    674      */
    675     private class SuggestListFocusListener implements OnFocusChangeListener {
    676         public void onFocusChange(View v, boolean focused) {
    677             if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
    678             if (focused) {
    679                 // The suggestions list got focus, hide the input method
    680                 hideInputMethod();
    681             }
    682             onSuggestionListFocusChange(focused);
    683         }
    684     }
    685 
    686     private class QueryTextViewFocusListener implements OnFocusChangeListener {
    687         public void onFocusChange(View v, boolean focused) {
    688             if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
    689             if (focused) {
    690                 // The query box got focus, show the input method
    691                 showInputMethodForQuery();
    692             }
    693             onQueryTextViewFocusChange(focused);
    694         }
    695     }
    696 
    697     private int getMaxSuggestions() {
    698         Config config = getConfig();
    699         return mCorpus == null
    700                 ? config.getMaxPromotedSuggestions()
    701                 : config.getMaxResultsPerSource();
    702     }
    703 
    704     private void updateSuggestionsBuffered() {
    705         mHandler.removeCallbacks(mUpdateSuggestionsTask);
    706         long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
    707         mHandler.postDelayed(mUpdateSuggestionsTask, delay);
    708     }
    709 
    710     protected void updateSuggestions(String query) {
    711 
    712         query = CharMatcher.WHITESPACE.trimLeadingFrom(query);
    713         if (DBG) Log.d(TAG, "getSuggestions(\""+query+"\","+mCorpus + ","+getMaxSuggestions()+")");
    714         Suggestions suggestions = getSuggestionsProvider().getSuggestions(
    715                 query, mCorpus, getMaxSuggestions());
    716 
    717         // Log start latency if this is the first suggestions update
    718         if (mStarting) {
    719             mStarting = false;
    720             String source = getIntent().getStringExtra(Search.SOURCE);
    721             int latency = mStartLatencyTracker.getLatency();
    722             getLogger().logStart(latency, source, mCorpus, suggestions.getExpectedCorpora());
    723             getQsbApplication().onStartupComplete();
    724         }
    725 
    726         mSuggestionsAdapter.setSuggestions(suggestions);
    727     }
    728 
    729     /**
    730      * If the input method is in fullscreen mode, and the selector corpus
    731      * is All or Web, use the web search suggestions as completions.
    732      */
    733     protected void updateInputMethodSuggestions() {
    734         InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
    735         if (imm == null || !imm.isFullscreenMode()) return;
    736         Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
    737         if (suggestions == null) return;
    738         SuggestionCursor cursor = suggestions.getPromoted();
    739         if (cursor == null) return;
    740         CompletionInfo[] completions = webSuggestionsToCompletions(cursor);
    741         if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
    742         imm.displayCompletions(mQueryTextView, completions);
    743     }
    744 
    745     private CompletionInfo[] webSuggestionsToCompletions(SuggestionCursor cursor) {
    746         int count = cursor.getCount();
    747         ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
    748         boolean usingWebCorpus = isSearchCorpusWeb();
    749         for (int i = 0; i < count; i++) {
    750             cursor.moveTo(i);
    751             if (!usingWebCorpus || cursor.isWebSearchSuggestion()) {
    752                 String text1 = cursor.getSuggestionText1();
    753                 completions.add(new CompletionInfo(i, i, text1));
    754             }
    755         }
    756         return completions.toArray(new CompletionInfo[completions.size()]);
    757     }
    758 
    759     private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
    760         if (!event.isSystem() && !isDpadKey(keyCode)) {
    761             if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
    762             if (mQueryTextView.requestFocus()) {
    763                 return mQueryTextView.dispatchKeyEvent(event);
    764             }
    765         }
    766         return false;
    767     }
    768 
    769     private boolean isDpadKey(int keyCode) {
    770         switch (keyCode) {
    771             case KeyEvent.KEYCODE_DPAD_UP:
    772             case KeyEvent.KEYCODE_DPAD_DOWN:
    773             case KeyEvent.KEYCODE_DPAD_LEFT:
    774             case KeyEvent.KEYCODE_DPAD_RIGHT:
    775             case KeyEvent.KEYCODE_DPAD_CENTER:
    776                 return true;
    777             default:
    778                 return false;
    779         }
    780     }
    781 
    782     /**
    783      * Filters the suggestions list when the search text changes.
    784      */
    785     private class SearchTextWatcher implements TextWatcher {
    786         public void afterTextChanged(Editable s) {
    787             boolean empty = s.length() == 0;
    788             if (empty != mQueryWasEmpty) {
    789                 mQueryWasEmpty = empty;
    790                 updateUi(empty);
    791             }
    792             if (mUpdateSuggestions) {
    793                 updateSuggestionsBuffered();
    794             }
    795         }
    796 
    797         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    798         }
    799 
    800         public void onTextChanged(CharSequence s, int start, int before, int count) {
    801         }
    802     }
    803 
    804     /**
    805      * Handles non-text keys in the query text view.
    806      */
    807     private class QueryTextViewKeyListener implements View.OnKeyListener {
    808         public boolean onKey(View view, int keyCode, KeyEvent event) {
    809             // Handle IME search action key
    810             if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
    811                 // if no action was taken, consume the key event so that the keyboard
    812                 // remains on screen.
    813                 return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
    814             }
    815             return false;
    816         }
    817     }
    818 
    819     /**
    820      * Handles key events on the search and voice search buttons,
    821      * by refocusing to EditText.
    822      */
    823     private class ButtonsKeyListener implements View.OnKeyListener {
    824         public boolean onKey(View v, int keyCode, KeyEvent event) {
    825             return forwardKeyToQueryTextView(keyCode, event);
    826         }
    827     }
    828 
    829     /**
    830      * Handles key events on the suggestions list view.
    831      */
    832     private class SuggestionsViewKeyListener implements View.OnKeyListener {
    833         public boolean onKey(View v, int keyCode, KeyEvent event) {
    834             if (event.getAction() == KeyEvent.ACTION_DOWN) {
    835                 int position = getSelectedPosition();
    836                 if (onSuggestionKeyDown(position, keyCode, event)) {
    837                     return true;
    838                 }
    839             }
    840             return forwardKeyToQueryTextView(keyCode, event);
    841         }
    842     }
    843 
    844     private class InputMethodCloser implements SuggestionsView.OnScrollListener {
    845 
    846         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    847                 int totalItemCount) {
    848         }
    849 
    850         public void onScrollStateChanged(AbsListView view, int scrollState) {
    851             hideInputMethod();
    852         }
    853     }
    854 
    855     private class ClickHandler implements SuggestionClickListener {
    856        public void onSuggestionClicked(int position) {
    857            launchSuggestion(position);
    858        }
    859 
    860        public void onSuggestionQuickContactClicked(int position) {
    861            clickedQuickContact(position);
    862        }
    863 
    864        public boolean onSuggestionLongClicked(int position) {
    865            return SearchActivity.this.onSuggestionLongClicked(position);
    866        }
    867 
    868        public void onSuggestionQueryRefineClicked(int position) {
    869            refineSuggestion(position);
    870        }
    871     }
    872 
    873     /**
    874      * Listens for clicks on the source selector.
    875      */
    876     private class SearchGoButtonClickListener implements View.OnClickListener {
    877         public void onClick(View view) {
    878             onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
    879         }
    880     }
    881 
    882     /**
    883      * Listens for clicks on the search button.
    884      */
    885     private class CorpusIndicatorClickListener implements View.OnClickListener {
    886         public void onClick(View view) {
    887             showCorpusSelectionDialog();
    888         }
    889     }
    890 
    891     private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
    892         public void onDismiss(DialogInterface dialog) {
    893             if (DBG) Log.d(TAG, "Corpus selector dismissed");
    894             clearStartedIntoCorpusSelectionDialog();
    895         }
    896     }
    897 
    898     private class CorpusSelectionListener
    899             implements CorpusSelectionDialog.OnCorpusSelectedListener {
    900         public void onCorpusSelected(String corpusName) {
    901             setCorpus(corpusName);
    902             updateSuggestions(getQuery());
    903             mQueryTextView.requestFocus();
    904             showInputMethodForQuery();
    905         }
    906     }
    907 
    908     /**
    909      * Listens for clicks on the voice search button.
    910      */
    911     private class VoiceSearchButtonClickListener implements View.OnClickListener {
    912         public void onClick(View view) {
    913             onVoiceSearchClicked();
    914         }
    915     }
    916 
    917     private class CorporaObserver extends DataSetObserver {
    918         @Override
    919         public void onChanged() {
    920             setCorpus(getCorpusName());
    921             updateSuggestions(getQuery());
    922         }
    923     }
    924 
    925     private class SuggestionsObserver extends DataSetObserver {
    926         @Override
    927         public void onChanged() {
    928             updateInputMethodSuggestions();
    929         }
    930     }
    931 
    932 }
    933