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 android.app.Activity;
     20 import android.app.SearchManager;
     21 import android.content.Intent;
     22 import android.net.Uri;
     23 import android.os.Bundle;
     24 import android.os.Debug;
     25 import android.os.Handler;
     26 import android.text.TextUtils;
     27 import android.util.Log;
     28 import android.view.Menu;
     29 import android.view.View;
     30 
     31 import com.android.common.Search;
     32 import com.android.quicksearchbox.ui.SearchActivityView;
     33 import com.android.quicksearchbox.ui.SuggestionClickListener;
     34 import com.android.quicksearchbox.ui.SuggestionsAdapter;
     35 import com.google.common.annotations.VisibleForTesting;
     36 import com.google.common.base.CharMatcher;
     37 
     38 import java.io.File;
     39 
     40 /**
     41  * The main activity for Quick Search Box. Shows the search UI.
     42  *
     43  */
     44 public class SearchActivity extends Activity {
     45 
     46     private static final boolean DBG = false;
     47     private static final String TAG = "QSB.SearchActivity";
     48 
     49     private static final String SCHEME_CORPUS = "qsb.corpus";
     50 
     51     private static final String INTENT_EXTRA_TRACE_START_UP = "trace_start_up";
     52 
     53     // Keys for the saved instance state.
     54     private static final String INSTANCE_KEY_QUERY = "query";
     55 
     56     private static final String ACTIVITY_HELP_CONTEXT = "search";
     57 
     58     private boolean mTraceStartUp;
     59     // Measures time from for last onCreate()/onNewIntent() call.
     60     private LatencyTracker mStartLatencyTracker;
     61     // Measures time spent inside onCreate()
     62     private LatencyTracker mOnCreateTracker;
     63     private int mOnCreateLatency;
     64     // Whether QSB is starting. True between the calls to onCreate()/onNewIntent() and onResume().
     65     private boolean mStarting;
     66     // True if the user has taken some action, e.g. launching a search, voice search,
     67     // or suggestions, since QSB was last started.
     68     private boolean mTookAction;
     69 
     70     private SearchActivityView mSearchActivityView;
     71 
     72     private Source mSource;
     73 
     74     private Bundle mAppSearchData;
     75 
     76     private final Handler mHandler = new Handler();
     77     private final Runnable mUpdateSuggestionsTask = new Runnable() {
     78         @Override
     79         public void run() {
     80             updateSuggestions();
     81         }
     82     };
     83 
     84     private final Runnable mShowInputMethodTask = new Runnable() {
     85         @Override
     86         public void run() {
     87             mSearchActivityView.showInputMethodForQuery();
     88         }
     89     };
     90 
     91     private OnDestroyListener mDestroyListener;
     92 
     93     /** Called when the activity is first created. */
     94     @Override
     95     public void onCreate(Bundle savedInstanceState) {
     96         mTraceStartUp = getIntent().hasExtra(INTENT_EXTRA_TRACE_START_UP);
     97         if (mTraceStartUp) {
     98             String traceFile = new File(getDir("traces", 0), "qsb-start.trace").getAbsolutePath();
     99             Log.i(TAG, "Writing start-up trace to " + traceFile);
    100             Debug.startMethodTracing(traceFile);
    101         }
    102         recordStartTime();
    103         if (DBG) Log.d(TAG, "onCreate()");
    104         super.onCreate(savedInstanceState);
    105 
    106         // This forces the HTTP request to check the users domain to be
    107         // sent as early as possible.
    108         QsbApplication.get(this).getSearchBaseUrlHelper();
    109 
    110         mSource = QsbApplication.get(this).getGoogleSource();
    111 
    112         mSearchActivityView = setupContentView();
    113 
    114         if (getConfig().showScrollingResults()) {
    115             mSearchActivityView.setMaxPromotedResults(getConfig().getMaxPromotedResults());
    116         } else {
    117             mSearchActivityView.limitResultsToViewHeight();
    118         }
    119 
    120         mSearchActivityView.setSearchClickListener(new SearchActivityView.SearchClickListener() {
    121             @Override
    122             public boolean onSearchClicked(int method) {
    123                 return SearchActivity.this.onSearchClicked(method);
    124             }
    125         });
    126 
    127         mSearchActivityView.setQueryListener(new SearchActivityView.QueryListener() {
    128             @Override
    129             public void onQueryChanged() {
    130                 updateSuggestionsBuffered();
    131             }
    132         });
    133 
    134         mSearchActivityView.setSuggestionClickListener(new ClickHandler());
    135 
    136         mSearchActivityView.setVoiceSearchButtonClickListener(new View.OnClickListener() {
    137             @Override
    138             public void onClick(View view) {
    139                 onVoiceSearchClicked();
    140             }
    141         });
    142 
    143         View.OnClickListener finishOnClick = new View.OnClickListener() {
    144             @Override
    145             public void onClick(View v) {
    146                 finish();
    147             }
    148         };
    149         mSearchActivityView.setExitClickListener(finishOnClick);
    150 
    151         // First get setup from intent
    152         Intent intent = getIntent();
    153         setupFromIntent(intent);
    154         // Then restore any saved instance state
    155         restoreInstanceState(savedInstanceState);
    156 
    157         // Do this at the end, to avoid updating the list view when setSource()
    158         // is called.
    159         mSearchActivityView.start();
    160 
    161         recordOnCreateDone();
    162     }
    163 
    164     protected SearchActivityView setupContentView() {
    165         setContentView(R.layout.search_activity);
    166         return (SearchActivityView) findViewById(R.id.search_activity_view);
    167     }
    168 
    169     protected SearchActivityView getSearchActivityView() {
    170         return mSearchActivityView;
    171     }
    172 
    173     @Override
    174     protected void onNewIntent(Intent intent) {
    175         if (DBG) Log.d(TAG, "onNewIntent()");
    176         recordStartTime();
    177         setIntent(intent);
    178         setupFromIntent(intent);
    179     }
    180 
    181     private void recordStartTime() {
    182         mStartLatencyTracker = new LatencyTracker();
    183         mOnCreateTracker = new LatencyTracker();
    184         mStarting = true;
    185         mTookAction = false;
    186     }
    187 
    188     private void recordOnCreateDone() {
    189         mOnCreateLatency = mOnCreateTracker.getLatency();
    190     }
    191 
    192     protected void restoreInstanceState(Bundle savedInstanceState) {
    193         if (savedInstanceState == null) return;
    194         String query = savedInstanceState.getString(INSTANCE_KEY_QUERY);
    195         setQuery(query, false);
    196     }
    197 
    198     @Override
    199     protected void onSaveInstanceState(Bundle outState) {
    200         super.onSaveInstanceState(outState);
    201         // We don't save appSearchData, since we always get the value
    202         // from the intent and the user can't change it.
    203 
    204         outState.putString(INSTANCE_KEY_QUERY, getQuery());
    205     }
    206 
    207     private void setupFromIntent(Intent intent) {
    208         if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
    209         String corpusName = getCorpusNameFromUri(intent.getData());
    210         String query = intent.getStringExtra(SearchManager.QUERY);
    211         Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
    212         boolean selectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
    213 
    214         setQuery(query, selectAll);
    215         mAppSearchData = appSearchData;
    216 
    217     }
    218 
    219     private String getCorpusNameFromUri(Uri uri) {
    220         if (uri == null) return null;
    221         if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
    222         return uri.getAuthority();
    223     }
    224 
    225     private QsbApplication getQsbApplication() {
    226         return QsbApplication.get(this);
    227     }
    228 
    229     private Config getConfig() {
    230         return getQsbApplication().getConfig();
    231     }
    232 
    233     protected SearchSettings getSettings() {
    234         return getQsbApplication().getSettings();
    235     }
    236 
    237     private SuggestionsProvider getSuggestionsProvider() {
    238         return getQsbApplication().getSuggestionsProvider();
    239     }
    240 
    241     private Logger getLogger() {
    242         return getQsbApplication().getLogger();
    243     }
    244 
    245     @VisibleForTesting
    246     public void setOnDestroyListener(OnDestroyListener l) {
    247         mDestroyListener = l;
    248     }
    249 
    250     @Override
    251     protected void onDestroy() {
    252         if (DBG) Log.d(TAG, "onDestroy()");
    253         mSearchActivityView.destroy();
    254         super.onDestroy();
    255         if (mDestroyListener != null) {
    256             mDestroyListener.onDestroyed();
    257         }
    258     }
    259 
    260     @Override
    261     protected void onStop() {
    262         if (DBG) Log.d(TAG, "onStop()");
    263         if (!mTookAction) {
    264             // TODO: This gets logged when starting other activities, e.g. by opening the search
    265             // settings, or clicking a notification in the status bar.
    266             // TODO we should log both sets of suggestions in 2-pane mode
    267             getLogger().logExit(getCurrentSuggestions(), getQuery().length());
    268         }
    269         // Close all open suggestion cursors. The query will be redone in onResume()
    270         // if we come back to this activity.
    271         mSearchActivityView.clearSuggestions();
    272         mSearchActivityView.onStop();
    273         super.onStop();
    274     }
    275 
    276     @Override
    277     protected void onPause() {
    278         if (DBG) Log.d(TAG, "onPause()");
    279         mSearchActivityView.onPause();
    280         super.onPause();
    281     }
    282 
    283     @Override
    284     protected void onRestart() {
    285         if (DBG) Log.d(TAG, "onRestart()");
    286         super.onRestart();
    287     }
    288 
    289     @Override
    290     protected void onResume() {
    291         if (DBG) Log.d(TAG, "onResume()");
    292         super.onResume();
    293         updateSuggestionsBuffered();
    294         mSearchActivityView.onResume();
    295         if (mTraceStartUp) Debug.stopMethodTracing();
    296     }
    297 
    298     @Override
    299     public boolean onPrepareOptionsMenu(Menu menu) {
    300         // Since the menu items are dynamic, we recreate the menu every time.
    301         menu.clear();
    302         createMenuItems(menu, true);
    303         return true;
    304     }
    305 
    306     public void createMenuItems(Menu menu, boolean showDisabled) {
    307         getQsbApplication().getHelp().addHelpMenuItem(menu, ACTIVITY_HELP_CONTEXT);
    308     }
    309 
    310     @Override
    311     public void onWindowFocusChanged(boolean hasFocus) {
    312         super.onWindowFocusChanged(hasFocus);
    313         if (hasFocus) {
    314             // Launch the IME after a bit
    315             mHandler.postDelayed(mShowInputMethodTask, 0);
    316         }
    317     }
    318 
    319     protected String getQuery() {
    320         return mSearchActivityView.getQuery();
    321     }
    322 
    323     protected void setQuery(String query, boolean selectAll) {
    324         mSearchActivityView.setQuery(query, selectAll);
    325     }
    326 
    327     /**
    328      * @return true if a search was performed as a result of this click, false otherwise.
    329      */
    330     protected boolean onSearchClicked(int method) {
    331         String query = CharMatcher.WHITESPACE.trimAndCollapseFrom(getQuery(), ' ');
    332         if (DBG) Log.d(TAG, "Search clicked, query=" + query);
    333 
    334         // Don't do empty queries
    335         if (TextUtils.getTrimmedLength(query) == 0) return false;
    336 
    337         mTookAction = true;
    338 
    339         // Log search start
    340         getLogger().logSearch(method, query.length());
    341 
    342         // Start search
    343         startSearch(mSource, query);
    344         return true;
    345     }
    346 
    347     protected void startSearch(Source searchSource, String query) {
    348         Intent intent = searchSource.createSearchIntent(query, mAppSearchData);
    349         launchIntent(intent);
    350     }
    351 
    352     protected void onVoiceSearchClicked() {
    353         if (DBG) Log.d(TAG, "Voice Search clicked");
    354 
    355         mTookAction = true;
    356 
    357         // Log voice search start
    358         getLogger().logVoiceSearch();
    359 
    360         // Start voice search
    361         Intent intent = mSource.createVoiceSearchIntent(mAppSearchData);
    362         launchIntent(intent);
    363     }
    364 
    365     protected Source getSearchSource() {
    366         return mSource;
    367     }
    368 
    369     protected SuggestionCursor getCurrentSuggestions() {
    370         Suggestions suggestions = mSearchActivityView.getSuggestions();
    371         if (suggestions == null) {
    372             return null;
    373         }
    374         return suggestions.getResult();
    375     }
    376 
    377     protected SuggestionPosition getCurrentSuggestions(SuggestionsAdapter<?> adapter, long id) {
    378         SuggestionPosition pos = adapter.getSuggestion(id);
    379         if (pos == null) {
    380             return null;
    381         }
    382         SuggestionCursor suggestions = pos.getCursor();
    383         int position = pos.getPosition();
    384         if (suggestions == null) {
    385             return null;
    386         }
    387         int count = suggestions.getCount();
    388         if (position < 0 || position >= count) {
    389             Log.w(TAG, "Invalid suggestion position " + position + ", count = " + count);
    390             return null;
    391         }
    392         suggestions.moveTo(position);
    393         return pos;
    394     }
    395 
    396     protected void launchIntent(Intent intent) {
    397         if (DBG) Log.d(TAG, "launchIntent " + intent);
    398         if (intent == null) {
    399             return;
    400         }
    401         try {
    402             startActivity(intent);
    403         } catch (RuntimeException ex) {
    404             // Since the intents for suggestions specified by suggestion providers,
    405             // guard against them not being handled, not allowed, etc.
    406             Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
    407         }
    408     }
    409 
    410     private boolean launchSuggestion(SuggestionsAdapter<?> adapter, long id) {
    411         SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
    412         if (suggestion == null) return false;
    413 
    414         if (DBG) Log.d(TAG, "Launching suggestion " + id);
    415         mTookAction = true;
    416 
    417         // Log suggestion click
    418         getLogger().logSuggestionClick(id, suggestion.getCursor(),
    419                 Logger.SUGGESTION_CLICK_TYPE_LAUNCH);
    420 
    421         // Launch intent
    422         launchSuggestion(suggestion.getCursor(), suggestion.getPosition());
    423 
    424         return true;
    425     }
    426 
    427     protected void launchSuggestion(SuggestionCursor suggestions, int position) {
    428         suggestions.moveTo(position);
    429         Intent intent = SuggestionUtils.getSuggestionIntent(suggestions, mAppSearchData);
    430         launchIntent(intent);
    431     }
    432 
    433     protected void refineSuggestion(SuggestionsAdapter<?> adapter, long id) {
    434         if (DBG) Log.d(TAG, "query refine clicked, pos " + id);
    435         SuggestionPosition suggestion = getCurrentSuggestions(adapter, id);
    436         if (suggestion == null) {
    437             return;
    438         }
    439         String query = suggestion.getSuggestionQuery();
    440         if (TextUtils.isEmpty(query)) {
    441             return;
    442         }
    443 
    444         // Log refine click
    445         getLogger().logSuggestionClick(id, suggestion.getCursor(),
    446                 Logger.SUGGESTION_CLICK_TYPE_REFINE);
    447 
    448         // Put query + space in query text view
    449         String queryWithSpace = query + ' ';
    450         setQuery(queryWithSpace, false);
    451         updateSuggestions();
    452         mSearchActivityView.focusQueryTextView();
    453     }
    454 
    455     private void updateSuggestionsBuffered() {
    456         if (DBG) Log.d(TAG, "updateSuggestionsBuffered()");
    457         mHandler.removeCallbacks(mUpdateSuggestionsTask);
    458         long delay = getConfig().getTypingUpdateSuggestionsDelayMillis();
    459         mHandler.postDelayed(mUpdateSuggestionsTask, delay);
    460     }
    461 
    462     private void gotSuggestions(Suggestions suggestions) {
    463         if (mStarting) {
    464             mStarting = false;
    465             String source = getIntent().getStringExtra(Search.SOURCE);
    466             int latency = mStartLatencyTracker.getLatency();
    467             getLogger().logStart(mOnCreateLatency, latency, source);
    468             getQsbApplication().onStartupComplete();
    469         }
    470     }
    471 
    472     public void updateSuggestions() {
    473         if (DBG) Log.d(TAG, "updateSuggestions()");
    474         final String query = CharMatcher.WHITESPACE.trimLeadingFrom(getQuery());
    475         updateSuggestions(query, mSource);
    476     }
    477 
    478     protected void updateSuggestions(String query, Source source) {
    479         if (DBG) Log.d(TAG, "updateSuggestions(\"" + query+"\"," + source + ")");
    480         Suggestions suggestions = getSuggestionsProvider().getSuggestions(
    481                 query, source);
    482 
    483         // Log start latency if this is the first suggestions update
    484         gotSuggestions(suggestions);
    485 
    486         showSuggestions(suggestions);
    487     }
    488 
    489     protected void showSuggestions(Suggestions suggestions) {
    490         mSearchActivityView.setSuggestions(suggestions);
    491     }
    492 
    493     private class ClickHandler implements SuggestionClickListener {
    494 
    495         @Override
    496         public void onSuggestionClicked(SuggestionsAdapter<?> adapter, long id) {
    497             launchSuggestion(adapter, id);
    498         }
    499 
    500         @Override
    501         public void onSuggestionQueryRefineClicked(SuggestionsAdapter<?> adapter, long id) {
    502             refineSuggestion(adapter, id);
    503         }
    504     }
    505 
    506     public interface OnDestroyListener {
    507         void onDestroyed();
    508     }
    509 
    510 }
    511