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.SearchActivityView;
     21 import com.android.quicksearchbox.ui.SuggestionClickListener;
     22 import com.android.quicksearchbox.ui.SuggestionsAdapter;
     23 import com.android.quicksearchbox.util.Consumer;
     24 import com.android.quicksearchbox.util.Consumers;
     25 import com.google.common.annotations.VisibleForTesting;
     26 import com.google.common.base.CharMatcher;
     27 
     28 import android.app.Activity;
     29 import android.app.AlertDialog;
     30 import android.app.SearchManager;
     31 import android.content.DialogInterface;
     32 import android.content.Intent;
     33 import android.database.DataSetObserver;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.os.Debug;
     37 import android.os.Handler;
     38 import android.text.TextUtils;
     39 import android.util.Log;
     40 import android.view.Menu;
     41 import android.view.View;
     42 import android.widget.Toast;
     43 
     44 import java.io.File;
     45 import java.util.ArrayList;
     46 import java.util.Collection;
     47 import java.util.List;
     48 import java.util.Set;
     49 
     50 /**
     51  * The main activity for Quick Search Box. Shows the search UI.
     52  *
     53  */
     54 public class SearchActivity extends Activity {
     55 
     56     private static final boolean DBG = false;
     57     private static final String TAG = "QSB.SearchActivity";
     58 
     59     private static final String SCHEME_CORPUS = "qsb.corpus";
     60 
     61     public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS
     62             = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS";
     63 
     64     private static final String INTENT_EXTRA_TRACE_START_UP = "trace_start_up";
     65 
     66     // Keys for the saved instance state.
     67     private static final String INSTANCE_KEY_CORPUS = "corpus";
     68     private static final String INSTANCE_KEY_QUERY = "query";
     69 
     70     private static final String ACTIVITY_HELP_CONTEXT = "search";
     71 
     72     private boolean mTraceStartUp;
     73     // Measures time from for last onCreate()/onNewIntent() call.
     74     private LatencyTracker mStartLatencyTracker;
     75     // Measures time spent inside onCreate()
     76     private LatencyTracker mOnCreateTracker;
     77     private int mOnCreateLatency;
     78     // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume().
     79     private boolean mStarting;
     80     // True if the user has taken some action, e.g. launching a search, voice search,
     81     // or suggestions, since QSB was last started.
     82     private boolean mTookAction;
     83 
     84     private SearchActivityView mSearchActivityView;
     85 
     86     private CorporaObserver mCorporaObserver;
     87 
     88     private Bundle mAppSearchData;
     89 
     90     private final Handler mHandler = new Handler();
     91     private final Runnable mUpdateSuggestionsTask = new Runnable() {
     92         public void run() {
     93             updateSuggestions();
     94         }
     95     };
     96 
     97     private final Runnable mShowInputMethodTask = new Runnable() {
     98         public void run() {
     99             mSearchActivityView.showInputMethodForQuery();
    100         }
    101     };
    102 
    103     private OnDestroyListener mDestroyListener;
    104 
    105     /** Called when the activity is first created. */
    106     @Override
    107     public void onCreate(Bundle savedInstanceState) {
    108         mTraceStartUp = getIntent().hasExtra(INTENT_EXTRA_TRACE_START_UP);
    109         if (mTraceStartUp) {
    110             String traceFile = new File(getDir("traces", 0), "qsb-start.trace").getAbsolutePath();
    111             Log.i(TAG, "Writing start-up trace to " + traceFile);
    112             Debug.startMethodTracing(traceFile);
    113         }
    114         recordStartTime();
    115         if (DBG) Log.d(TAG, "onCreate()");
    116         super.onCreate(savedInstanceState);
    117 
    118         // This forces the HTTP request to check the users domain to be
    119         // sent as early as possible.
    120         QsbApplication.get(this).getSearchBaseUrlHelper();
    121 
    122         mSearchActivityView = setupContentView();
    123 
    124         if (getConfig().showScrollingSuggestions()) {
    125             mSearchActivityView.setMaxPromotedSuggestions(getConfig().getMaxPromotedSuggestions());
    126         } else {
    127             mSearchActivityView.limitSuggestionsToViewHeight();
    128         }
    129         if (getConfig().showScrollingResults()) {
    130             mSearchActivityView.setMaxPromotedResults(getConfig().getMaxPromotedResults());
    131         } else {
    132             mSearchActivityView.limitResultsToViewHeight();
    133         }
    134 
    135         mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() {
    136             public boolean onSearchClicked(int method) {
    137                 return SearchActivity.this.onSearchClicked(method);
    138             }
    139         });
    140 
    141         mSearchActivityView.setQueryListener(new SearchActivityView.QueryListener() {
    142             public void onQueryChanged() {
    143                 updateSuggestionsBuffered();
    144             }
    145         });
    146 
    147         mSearchActivityView.setSuggestionClickListener(new ClickHandler());
    148 
    149         mSearchActivityView.setVoiceSearchButtonClickListener(new View.OnClickListener() {
    150             public void onClick(View view) {
    151                 onVoiceSearchClicked();
    152             }
    153         });
    154 
    155         View.OnClickListener finishOnClick = new View.OnClickListener() {
    156             public void onClick(View v) {
    157                 finish();
    158             }
    159         };
    160         mSearchActivityView.setExitClickListener(finishOnClick);
    161 
    162         // First get setup from intent
    163         Intent intent = getIntent();
    164         setupFromIntent(intent);
    165         // Then restore any saved instance state
    166         restoreInstanceState(savedInstanceState);
    167 
    168         // Do this at the end, to avoid updating the list view when setSource()
    169         // is called.
    170         mSearchActivityView.start();
    171 
    172         mCorporaObserver = new CorporaObserver();
    173         getCorpora().registerDataSetObserver(mCorporaObserver);
    174         recordOnCreateDone();
    175     }
    176 
    177     protected SearchActivityView setupContentView() {
    178         setContentView(R.layout.search_activity);
    179         return (SearchActivityView) findViewById(R.id.search_activity_view);
    180     }
    181 
    182     protected SearchActivityView getSearchActivityView() {
    183         return mSearchActivityView;
    184     }
    185 
    186     @Override
    187     protected void onNewIntent(Intent intent) {
    188         if (DBG) Log.d(TAG, "onNewIntent()");
    189         recordStartTime();
    190         setIntent(intent);
    191         setupFromIntent(intent);
    192     }
    193 
    194     private void recordStartTime() {
    195         mStartLatencyTracker = new LatencyTracker();
    196         mOnCreateTracker = new LatencyTracker();
    197         mStarting = true;
    198         mTookAction = false;
    199     }
    200 
    201     private void recordOnCreateDone() {
    202         mOnCreateLatency = mOnCreateTracker.getLatency();
    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             mSearchActivityView.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     public 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() {
    271         return mSearchActivityView.getCorpus();
    272     }
    273 
    274     private String getCorpusName() {
    275         return mSearchActivityView.getCorpusName();
    276     }
    277 
    278     private void setCorpus(String name) {
    279         mSearchActivityView.setCorpus(name);
    280     }
    281 
    282     private QsbApplication getQsbApplication() {
    283         return QsbApplication.get(this);
    284     }
    285 
    286     private Config getConfig() {
    287         return getQsbApplication().getConfig();
    288     }
    289 
    290     protected SearchSettings getSettings() {
    291         return getQsbApplication().getSettings();
    292     }
    293 
    294     private Corpora getCorpora() {
    295         return getQsbApplication().getCorpora();
    296     }
    297 
    298     private CorpusRanker getCorpusRanker() {
    299         return getQsbApplication().getCorpusRanker();
    300     }
    301 
    302     private ShortcutRepository getShortcutRepository() {
    303         return getQsbApplication().getShortcutRepository();
    304     }
    305 
    306     private SuggestionsProvider getSuggestionsProvider() {
    307         return getQsbApplication().getSuggestionsProvider();
    308     }
    309 
    310     private Logger getLogger() {
    311         return getQsbApplication().getLogger();
    312     }
    313 
    314     @VisibleForTesting
    315     public void setOnDestroyListener(OnDestroyListener l) {
    316         mDestroyListener = l;
    317     }
    318 
    319     @Override
    320     protected void onDestroy() {
    321         if (DBG) Log.d(TAG, "onDestroy()");
    322         getCorpora().unregisterDataSetObserver(mCorporaObserver);
    323         mSearchActivityView.destroy();
    324         super.onDestroy();
    325         if (mDestroyListener != null) {
    326             mDestroyListener.onDestroyed();
    327         }
    328     }
    329 
    330     @Override
    331     protected void onStop() {
    332         if (DBG) Log.d(TAG, "onStop()");
    333         if (!mTookAction) {
    334             // TODO: This gets logged when starting other activities, e.g. by opening the search
    335             // settings, or clicking a notification in the status bar.
    336             // TODO we should log both sets of suggestions in 2-pane mode
    337             getLogger().logExit(getCurrentSuggestions(), getQuery().length());
    338         }
    339         // Close all open suggestion cursors. The query will be redone in onResume()
    340         // if we come back to this activity.
    341         mSearchActivityView.clearSuggestions();
    342         getQsbApplication().getShortcutRefresher().reset();
    343         mSearchActivityView.onStop();
    344         super.onStop();
    345     }
    346 
    347     @Override
    348     protected void onPause() {
    349         if (DBG) Log.d(TAG, "onPause()");
    350         mSearchActivityView.onPause();
    351         super.onPause();
    352     }
    353 
    354     @Override
    355     protected void onRestart() {
    356         if (DBG) Log.d(TAG, "onRestart()");
    357         super.onRestart();
    358     }
    359 
    360     @Override
    361     protected void onResume() {
    362         if (DBG) Log.d(TAG, "onResume()");
    363         super.onResume();
    364         updateSuggestionsBuffered();
    365         mSearchActivityView.onResume();
    366         if (mTraceStartUp) Debug.stopMethodTracing();
    367     }
    368 
    369     @Override
    370     public boolean onPrepareOptionsMenu(Menu menu) {
    371         // Since the menu items are dynamic, we recreate the menu every time.
    372         menu.clear();
    373         createMenuItems(menu, true);
    374         return true;
    375     }
    376 
    377     public void createMenuItems(Menu menu, boolean showDisabled) {
    378         getSettings().addMenuItems(menu, showDisabled);
    379         getQsbApplication().getHelp().addHelpMenuItem(menu, ACTIVITY_HELP_CONTEXT);
    380     }
    381 
    382     @Override
    383     public void onWindowFocusChanged(boolean hasFocus) {
    384         super.onWindowFocusChanged(hasFocus);
    385         if (hasFocus) {
    386             // Launch the IME after a bit
    387             mHandler.postDelayed(mShowInputMethodTask, 0);
    388         }
    389     }
    390 
    391     protected String getQuery() {
    392         return mSearchActivityView.getQuery();
    393     }
    394 
    395     protected void setQuery(String query, boolean selectAll) {
    396         mSearchActivityView.setQuery(query, selectAll);
    397     }
    398 
    399     public CorpusSelectionDialog getCorpusSelectionDialog() {
    400         CorpusSelectionDialog dialog = createCorpusSelectionDialog();
    401         dialog.setOwnerActivity(this);
    402         dialog.setOnDismissListener(new CorpusSelectorDismissListener());
    403         return dialog;
    404     }
    405 
    406     protected CorpusSelectionDialog createCorpusSelectionDialog() {
    407         return new CorpusSelectionDialog(this, getSettings());
    408     }
    409 
    410     /**
    411      * @return true if a search was performed as a result of this click, false otherwise.
    412      */
    413     protected boolean onSearchClicked(int method) {
    414         String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' ');
    415         if (DBG) Log.d(TAG, "Search clicked, query=" + query);
    416 
    417         // Don't do empty queries
    418         if (TextUtils.getTrimmedLength(query) == 0) return false;
    419 
    420         Corpus searchCorpus = getSearchCorpus();
    421         if (searchCorpus == null) return false;
    422 
    423         mTookAction = true;
    424 
    425         // Log search start
    426         getLogger().logSearch(getCorpus(), method, query.length());
    427 
    428         // Start search
    429         startSearch(searchCorpus, query);
    430         return true;
    431     }
    432 
    433     protected void startSearch(Corpus searchCorpus, String query) {
    434         Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
    435         launchIntent(intent);
    436     }
    437 
    438     protected void onVoiceSearchClicked() {
    439         if (DBG) Log.d(TAG, "Voice Search clicked");
    440         Corpus searchCorpus = getSearchCorpus();
    441         if (searchCorpus == null) return;
    442 
    443         mTookAction = true;
    444 
    445         // Log voice search start
    446         getLogger().logVoiceSearch(searchCorpus);
    447 
    448         // Start voice search
    449         Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
    450         launchIntent(intent);
    451     }
    452 
    453     protected Corpus getSearchCorpus() {
    454         return mSearchActivityView.getSearchCorpus();
    455     }
    456 
    457     protected SuggestionCursor getCurrentSuggestions() {
    458         return mSearchActivityView.getCurrentPromotedSuggestions();
    459     }
    460 
    461     protected SuggestionPosition getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id) {
    462         SuggestionPosition pos = adapter.getSuggestion(id);
    463         if (pos == null) {
    464             return null;
    465         }
    466         SuggestionCursor suggestions = pos.getCursor();
    467         int position = pos.getPosition();
    468         if (suggestions == null) {
    469             return null;
    470         }
    471         int count = suggestions.getCount();
    472         if (position < 0 || position >= count) {
    473             Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count);
    474             return null;
    475         }
    476         suggestions.moveTo(position);
    477         return pos;
    478     }
    479 
    480     protected Set<Corpus> getCurrentIncludedCorpora() {
    481         Suggestions suggestions = mSearchActivityView.getSuggestions();
    482         return suggestions == null  ? null : suggestions.getIncludedCorpora();
    483     }
    484 
    485     protected void launchIntent(Intent intent) {
    486         if (DBG) Log.d(TAG, "launchIntent " + intent);
    487         if (intent == null) {
    488             return;
    489         }
    490         try {
    491             startActivity(intent);
    492         } catch (RuntimeException ex) {
    493             // Since the intents for suggestions specified by suggestion providers,
    494             // guard against them not being handled, not allowed, etc.
    495             Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
    496         }
    497     }
    498 
    499     private boolean launchSuggestion(SuggestionsAdapter<?> adapter, long id) {
    500         SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
    501         if (suggestion == null) return false;
    502 
    503         if (DBG) Log.d(TAG, "Launching suggestion " + id);
    504         mTookAction = true;
    505 
    506         // Log suggestion click
    507         getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
    508                 Logger.SUGGESTION_CLICK_TYPE_LAUNCH);
    509 
    510         // Create shortcut
    511         getShortcutRepository().reportClick(suggestion.getCursor(), suggestion.getPosition());
    512 
    513         // Launch intent
    514         launchSuggestion(suggestion.getCursor(), suggestion.getPosition());
    515 
    516         return true;
    517     }
    518 
    519     protected void launchSuggestion(SuggestionCursor suggestions, int position) {
    520         suggestions.moveTo(position);
    521         Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData);
    522         launchIntent(intent);
    523     }
    524 
    525     protected void removeFromHistoryClicked(final SuggestionsAdapter<?> adapter,
    526             final long id) {
    527         SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
    528         if (suggestion == null) return;
    529         CharSequence title = suggestion.getSuggestionText1();
    530         AlertDialog dialog = new AlertDialog.Builder(this)
    531                 .setTitle(title)
    532                 .setMessage(R.string.remove_from_history)
    533                 .setPositiveButton(android.R.string.ok,
    534                         new DialogInterface.OnClickListener() {
    535                             public void onClick(DialogInterface dialog, int which) {
    536                                 // TODO: what if the suggestions have changed?
    537                                 removeFromHistory(adapter, id);
    538                             }
    539                         })
    540                 .setNegativeButton(android.R.string.cancel, null)
    541                 .create();
    542         dialog.show();
    543     }
    544 
    545     protected void removeFromHistory(SuggestionsAdapter<?> adapter, long id) {
    546         SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
    547         if (suggestion == null) return;
    548         removeFromHistory(suggestion.getCursor(), suggestion.getPosition());
    549         // TODO: Log to event log?
    550     }
    551 
    552     protected void removeFromHistory(SuggestionCursor suggestions, int position) {
    553         removeShortcut(suggestions, position);
    554         removeFromHistoryDone(true);
    555     }
    556 
    557     protected void removeFromHistoryDone(boolean ok) {
    558         Log.i(TAG, "Removed query from history, success=" + ok);
    559         updateSuggestionsBuffered();
    560         if (!ok) {
    561             Toast.makeText(this, R.string.remove_from_history_failed, Toast.LENGTH_SHORT).show();
    562         }
    563     }
    564 
    565     protected void removeShortcut(SuggestionCursor suggestions, int position) {
    566         if (suggestions.isSuggestionShortcut()) {
    567             if (DBG) Log.d(TAG, "Removing suggestion " + position + " from shortcuts");
    568             getShortcutRepository().removeFromHistory(suggestions, position);
    569         }
    570     }
    571 
    572     protected void clickedQuickContact(SuggestionsAdapter<?> adapter, long id) {
    573         SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
    574         if (suggestion == null) return;
    575 
    576         if (DBG) Log.d(TAG, "Used suggestion " + suggestion.getPosition());
    577         mTookAction = true;
    578 
    579         // Log suggestion click
    580         getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
    581                 Logger.SUGGESTION_CLICK_TYPE_QUICK_CONTACT);
    582 
    583         // Create shortcut
    584         getShortcutRepository().reportClick(suggestion.getCursor(), suggestion.getPosition());
    585     }
    586 
    587     protected void refineSuggestion(SuggestionsAdapter<?> adapter, long id) {
    588         if (DBG) Log.d(TAG, "query refine clicked, pos " + id);
    589         SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
    590         if (suggestion == null) {
    591             return;
    592         }
    593         String query = suggestion.getSuggestionQuery();
    594         if (TextUtils.isEmpty(query)) {
    595             return;
    596         }
    597 
    598         // Log refine click
    599         getLogger().logSuggestionClick(id, suggestion.getCursor(), getCurrentIncludedCorpora(),
    600                 Logger.SUGGESTION_CLICK_TYPE_REFINE);
    601 
    602         // Put query + space in query text view
    603         String queryWithSpace = query + ' ';
    604         setQuery(queryWithSpace, false);
    605         updateSuggestions();
    606         mSearchActivityView.focusQueryTextView();
    607     }
    608 
    609     private void updateSuggestionsBuffered() {
    610         if (DBG) Log.d(TAG, "updateSuggestionsBuffered()");
    611         mHandler.removeCallbacks(mUpdateSuggestionsTask);
    612         long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
    613         mHandler.postDelayed(mUpdateSuggestionsTask, delay);
    614     }
    615 
    616     private void gotSuggestions(Suggestions suggestions) {
    617         if (mStarting) {
    618             mStarting = false;
    619             String source = getIntent().getStringExtra(Search.SOURCE);
    620             int latency = mStartLatencyTracker.getLatency();
    621             getLogger().logStart(mOnCreateLatency, latency, source, getCorpus(),
    622                     suggestions == null ? null : suggestions.getExpectedCorpora());
    623             getQsbApplication().onStartupComplete();
    624         }
    625     }
    626 
    627     private void getCorporaToQuery(Consumer<List<Corpus>> consumer) {
    628         Corpus corpus = getCorpus();
    629         if (corpus == null) {
    630             getCorpusRanker().getCorporaInAll(Consumers.createAsyncConsumer(mHandler, consumer));
    631         } else {
    632             List<Corpus> corpora = new ArrayList<Corpus>();
    633             Corpus searchCorpus = getSearchCorpus();
    634             if (searchCorpus != null) corpora.add(searchCorpus);
    635             consumer.consume(corpora);
    636         }
    637     }
    638 
    639     protected void getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery,
    640             final Suggestions suggestions) {
    641         ShortcutRepository shortcutRepo = getShortcutRepository();
    642         if (shortcutRepo == null) return;
    643         if (query.length() == 0 && !getConfig().showShortcutsForZeroQuery()) {
    644             return;
    645         }
    646         Consumer<ShortcutCursor> consumer = Consumers.createAsyncCloseableConsumer(mHandler,
    647                 new Consumer<ShortcutCursor>() {
    648             public boolean consume(ShortcutCursor shortcuts) {
    649                 suggestions.setShortcuts(shortcuts);
    650                 return true;
    651             }
    652         });
    653         shortcutRepo.getShortcutsForQuery(query, corporaToQuery,
    654                 getSettings().allowWebSearchShortcuts(), consumer);
    655     }
    656 
    657     public void updateSuggestions() {
    658         if (DBG) Log.d(TAG, "updateSuggestions()");
    659         final String query = CharMatcher.WHITESPACE.trimLeadingFrom(getQuery());
    660         getQsbApplication().getSourceTaskExecutor().cancelPendingTasks();
    661         getCorporaToQuery(new Consumer<List<Corpus>>(){
    662             @Override
    663             public boolean consume(List<Corpus> corporaToQuery) {
    664                 updateSuggestions(query, corporaToQuery);
    665                 return true;
    666             }
    667         });
    668     }
    669 
    670     protected void updateSuggestions(String query, List<Corpus> corporaToQuery) {
    671         if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + corporaToQuery + ")");
    672         Suggestions suggestions = getSuggestionsProvider().getSuggestions(
    673                 query, corporaToQuery);
    674         getShortcutsForQuery(query, corporaToQuery, suggestions);
    675 
    676         // Log start latency if this is the first suggestions update
    677         gotSuggestions(suggestions);
    678 
    679         showSuggestions(suggestions);
    680     }
    681 
    682     protected void showSuggestions(Suggestions suggestions) {
    683         mSearchActivityView.setSuggestions(suggestions);
    684     }
    685 
    686     private class ClickHandler implements SuggestionClickListener {
    687 
    688         public void onSuggestionQuickContactClicked(SuggestionsAdapter<?> adapter, long id) {
    689             clickedQuickContact(adapter, id);
    690         }
    691 
    692         public void onSuggestionClicked(SuggestionsAdapter<?> adapter, long id) {
    693             launchSuggestion(adapter, id);
    694         }
    695 
    696         public void onSuggestionRemoveFromHistoryClicked(SuggestionsAdapter<?> adapter, long id) {
    697             removeFromHistoryClicked(adapter, id);
    698         }
    699 
    700         public void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id) {
    701             refineSuggestion(adapter, id);
    702         }
    703     }
    704 
    705     private class CorpusSelectorDismissListener implements DialogInterface.OnDismissListener {
    706         public void onDismiss(DialogInterface dialog) {
    707             if (DBG) Log.d(TAG, "Corpus selector dismissed");
    708             clearStartedIntoCorpusSelectionDialog();
    709         }
    710     }
    711 
    712     private class CorporaObserver extends DataSetObserver {
    713         @Override
    714         public void onChanged() {
    715             setCorpus(getCorpusName());
    716             updateSuggestions();
    717         }
    718     }
    719 
    720     public interface OnDestroyListener {
    721         void onDestroyed();
    722     }
    723 
    724 }
    725