SearchActivity.java revision b32ee8f1b45c65daff18b40c0614cf18843b8c17
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
17package com.android.quicksearchbox;
18
19import android.app.Activity;
20import android.app.SearchManager;
21import android.content.Intent;
22import android.database.DataSetObserver;
23import android.graphics.Rect;
24import android.graphics.drawable.Animatable;
25import android.os.Bundle;
26import android.text.Editable;
27import android.text.TextUtils;
28import android.text.TextWatcher;
29import android.util.Log;
30import android.view.KeyEvent;
31import android.view.Menu;
32import android.view.View;
33import android.view.View.OnFocusChangeListener;
34import android.view.inputmethod.InputMethodManager;
35import android.widget.EditText;
36import android.widget.ImageButton;
37import android.widget.ProgressBar;
38import android.widget.ScrollView;
39
40// TODO: close / deactivate cursors in onPause() or onStop()
41// TODO: use permission to get extended Genie suggestions
42// TODO: show spinner until done
43// TODO: don't show new results until there is at least one, or it's done
44// TODO: add timeout for source queries
45// TODO: group refreshes that happen close to each other.
46// TODO: handle long clicks
47// TODO: search / voice search when button pressed
48// TODO: use queryAfterZeroResults()
49// TODO: support action keys
50// TODO: support IME search action
51// TODO: allow typing everywhere in the UI
52// TODO: support intent extras for source (e.g. launcher widget)
53// TODO: support intent extras for initial state, e.g. query, selection
54// TODO: make Config server-side configurable
55// TODO: add resourced for hi-res version, and fix layouts
56// TODO: log impressions
57// TODO: use source ranking
58// TODO: Show tabs at bottom too
59
60/**
61 * The main activity for Quick Search Box. Shows the search UI.
62 *
63 */
64public class SearchActivity extends Activity {
65
66    private static final boolean DBG = true;
67    private static final String TAG = "SearchActivity";
68
69    // TODO: This is hidden in SearchManager
70    public final static String INTENT_ACTION_SEARCH_SETTINGS
71            = "android.search.action.SEARCH_SETTINGS";
72
73    protected EditText mQueryTextView;
74
75    protected ScrollView mSuggestionsScrollView;
76    protected SuggestionsView mSuggestionsView;
77
78    protected ImageButton mSearchGoButton;
79    protected ImageButton mVoiceSearchButton;
80    protected ProgressBar mProgressBar;
81
82    private Launcher mLauncher;
83
84    private boolean mUpdateSuggestions = true;
85    private String mUserQuery = "";
86    private boolean mSelectAll = false;
87
88    /** Called when the activity is first created. */
89    @Override
90    public void onCreate(Bundle savedInstanceState) {
91        if (DBG) Log.d(TAG, "onCreate()");
92        // TODO: Use savedInstanceState to restore state
93        super.onCreate(savedInstanceState);
94        setContentView(R.layout.search_bar);
95
96        mQueryTextView = (EditText) findViewById(R.id.search_src_text);
97        mSuggestionsScrollView = (ScrollView) findViewById(R.id.suggestions_scroll);
98        mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
99
100        mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
101        mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
102
103        mQueryTextView.addTextChangedListener(new SearchTextWatcher());
104        mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
105        mQueryTextView.setOnFocusChangeListener(new SuggestListFocusListener());
106
107        mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
108        mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener());
109
110        Bundle appSearchData = null;
111
112        Intent intent = getIntent();
113        // getIntent() currently always returns non-null, but the API does not guarantee
114        // that it always will.
115        if (intent != null) {
116            String initialQuery = intent.getStringExtra(SearchManager.QUERY);
117            if (!TextUtils.isEmpty(initialQuery)) {
118                mUserQuery = initialQuery;
119            }
120            // TODO: Declare an intent extra for selectAll
121            appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
122        }
123        mLauncher = new Launcher(this, appSearchData);
124        mVoiceSearchButton.setVisibility(
125                mLauncher.isVoiceSearchAvailable() ? View.VISIBLE : View.GONE);
126
127        mSuggestionsView.setClickListener(new SuggestionsClickListener());
128    }
129
130    private QsbApplication getQsbApplication() {
131        return (QsbApplication) getApplication();
132    }
133
134    private ShortcutRepository getShortcutRepository() {
135        return getQsbApplication().getShortcutRepository();
136    }
137
138    private SuggestionsProvider getSuggestionsProvider() {
139        return getQsbApplication().getSuggestionsProvider();
140    }
141
142    @Override
143    protected void onDestroy() {
144        if (DBG) Log.d(TAG, "onDestroy()");
145        super.onDestroy();
146        if (mSuggestionsView != null) {
147            mSuggestionsView.close();
148            mSuggestionsView = null;
149        }
150    }
151
152    @Override
153    protected void onStart() {
154        if (DBG) Log.d(TAG, "onStart()");
155        super.onStart();
156        setQuery(mUserQuery, mSelectAll);
157        // Only select everything the first time after creating the activity.
158        mSelectAll = false;
159        updateSuggestions(mUserQuery);
160    }
161
162    @Override
163    protected void onStop() {
164        if (DBG) Log.d(TAG, "onResume()");
165        // Close all open suggestion cursors. The query will be redone in onStart()
166        // if we come back to this activity.
167        mSuggestionsView.setSuggestions(null);
168        super.onStop();
169    }
170
171    @Override
172    protected void onResume() {
173        super.onResume();
174        mQueryTextView.requestFocus();
175    }
176
177    @Override
178    public boolean onCreateOptionsMenu(Menu menu) {
179        super.onCreateOptionsMenu(menu);
180
181        Intent settings = new Intent(INTENT_ACTION_SEARCH_SETTINGS);
182        settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
183        // Don't show activity chooser if there are multiple search settings activities,
184        // e.g. from different QSB implementations.
185        settings.setPackage(this.getPackageName());
186        menu.add(Menu.NONE, Menu.NONE, 0, R.string.menu_settings)
187                .setIcon(android.R.drawable.ic_menu_preferences).setAlphabeticShortcut('P')
188                .setIntent(settings);
189
190        return true;
191    }
192
193    private String getQuery() {
194        CharSequence q = mQueryTextView.getText();
195        return q == null ? "" : q.toString();
196    }
197
198    /**
199     * Restores the query entered by the user.
200     */
201    private void restoreUserQuery() {
202        if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'");
203        setQuery(mUserQuery, false);
204    }
205
206    /**
207     * Sets the text in the query box. Does not update the suggestions,
208     * and does not change the saved user-entered query.
209     * {@link #restoreUserQuery()} will restore the query to the last
210     * user-entered query.
211     */
212    private void setQuery(String query, boolean selectAll) {
213        mUpdateSuggestions = false;
214        mQueryTextView.setText(query);
215        setTextSelection(selectAll);
216        mUpdateSuggestions = true;
217    }
218
219    /**
220     * Sets the text selection in the query text view.
221     *
222     * @param selectAll If {@code true}, selects the entire query.
223     *        If {@false}, no characters are selected, and the cursor is placed
224     *        at the end of the query.
225     */
226    private void setTextSelection(boolean selectAll) {
227        if (selectAll) {
228            mQueryTextView.setSelection(0, mQueryTextView.length());
229        } else {
230            mQueryTextView.setSelection(mQueryTextView.length());
231        }
232    }
233
234    protected void onSearchClicked() {
235        String query = getQuery();
236        if (DBG) Log.d(TAG, "Search clicked, query=" + query);
237        mLauncher.startWebSearch(query);
238    }
239
240    protected void onVoiceSearchClicked() {
241        if (DBG) Log.d(TAG, "Voice Search clicked");
242        mLauncher.startVoiceSearch();
243    }
244
245    protected boolean onSuggestionClicked(SuggestionPosition suggestion) {
246        if (DBG) Log.d(TAG, "Clicked on suggestion " + suggestion);
247        // TODO: handle action keys
248        mLauncher.launchSuggestion(suggestion, KeyEvent.KEYCODE_UNKNOWN, null);
249        getShortcutRepository().reportClick(suggestion);
250        // Update search widgets, since the top shortcuts can have changed.
251        SearchWidgetProvider.updateSearchWidgets(this);
252        return true;
253    }
254
255    protected boolean onIconClicked(SuggestionPosition suggestion, Rect target) {
256      if (DBG) Log.d(TAG, "Clicked on suggestion icon " + suggestion);
257      mLauncher.launchSuggestionSecondary(suggestion, target);
258      getShortcutRepository().reportClick(suggestion);
259      return true;
260    }
261
262    protected boolean onSuggestionLongClicked(SuggestionCursor sourceResult) {
263        if (DBG) Log.d(TAG, "Long clicked on suggestion " + sourceResult.getSuggestionText1());
264        return false;
265    }
266
267    protected void onSuggestionSelected(SuggestionCursor sourceResult) {
268        String displayQuery = sourceResult.getSuggestionDisplayQuery();
269        if (DBG) {
270            Log.d(TAG, "Selected suggestion " + sourceResult.getSuggestionText1()
271                    + ",displayQuery="+ displayQuery);
272        }
273        if (TextUtils.isEmpty(displayQuery)) {
274            restoreUserQuery();
275        } else {
276            setQuery(displayQuery, false);
277        }
278    }
279
280    protected void onSourceSelected() {
281        if (DBG) Log.d(TAG, "No suggestion selected");
282        restoreUserQuery();
283    }
284
285    @Override
286    public boolean onKeyUp(int keyCode, KeyEvent event) {
287        if (DBG) Log.d(TAG, "onKeyUp(" + event + ")");
288        return super.onKeyUp(keyCode, event);
289    }
290
291    /**
292     * Hides the input method.
293     */
294    protected void hideInputMethod() {
295        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
296        if (imm != null) {
297            imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0);
298        }
299    }
300
301    /**
302     * Hides the input method when the suggestions get focus.
303     */
304    private class SuggestListFocusListener implements OnFocusChangeListener {
305        public void onFocusChange(View v, boolean focused) {
306            if (v == mQueryTextView) {
307                if (!focused) {
308                    hideInputMethod();
309                } else {
310                    // TODO: clear list selection?
311                }
312            }
313        }
314    }
315
316    private void startSearchProgress() {
317        // TODO: Cache animation between calls?
318        mSearchGoButton.setImageResource(R.drawable.searching);
319        Animatable animation = (Animatable) mSearchGoButton.getDrawable();
320        animation.start();
321    }
322
323    private void stopSearchProgress() {
324        Animatable animation = (Animatable) mSearchGoButton.getDrawable();
325        // TODO: Is this needed, or is it done automatically when the
326        // animation is removed?
327        animation.stop();
328        mSearchGoButton.setImageResource(R.drawable.ic_btn_search);
329    }
330
331    private void scrollPastTabs() {
332        // TODO: Right after starting, the scroll view hasn't been measured,
333        // so it doesn't know whether its contents are tall enough to scroll.
334        int yOffset = mSuggestionsView.getTabHeight();
335        mSuggestionsScrollView.scrollTo(0, yOffset);
336        if (DBG) {
337            Log.d(TAG, "After scrollTo(0," + yOffset + "), scrollY="
338                    + mSuggestionsScrollView.getScrollY());
339        }
340    }
341
342    private void updateSuggestions(String query) {
343        LatencyTracker latency = new LatencyTracker(TAG);
344        Suggestions suggestions = getSuggestionsProvider().getSuggestions(query);
345        latency.addEvent("getSuggestions_done");
346        if (!suggestions.isDone()) {
347            suggestions.registerDataSetObserver(new ProgressUpdater(suggestions));
348            startSearchProgress();
349        }
350        mSuggestionsView.setSuggestions(suggestions);
351        scrollPastTabs();
352        latency.addEvent("shortcuts_shown");
353        long userVisibleLatency = latency.getUserVisibleLatency();
354        if (DBG) {
355            Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms.");
356        }
357    }
358
359    /**
360     * Filters the suggestions list when the search text changes.
361     */
362    private class SearchTextWatcher implements TextWatcher {
363        public void afterTextChanged(Editable s) {
364            if (mUpdateSuggestions) {
365                String query = s == null ? "" : s.toString();
366                mUserQuery = query;
367                updateSuggestions(query);
368            }
369        }
370
371        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
372        }
373
374        public void onTextChanged(CharSequence s, int start, int before, int count) {
375        }
376    }
377
378    /**
379     * Handles non-text keys in the query text view.
380     */
381    private class QueryTextViewKeyListener implements View.OnKeyListener {
382        public boolean onKey(View view, int keyCode, KeyEvent event) {
383            if (DBG) Log.d(TAG, "QueryTextViewKeyListener.onKey(" + event + ")");
384            // Handle IME search action key
385            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
386                onSearchClicked();
387            }
388            return false;
389        }
390    }
391
392    private class SuggestionsClickListener implements SuggestionsView.ClickListener {
393       public void onIconClicked(SuggestionPosition suggestion, Rect rect) {
394           SearchActivity.this.onIconClicked(suggestion, rect);
395       }
396
397       public void onItemClicked(SuggestionPosition suggestion) {
398           SearchActivity.this.onSuggestionClicked(suggestion);
399       }
400
401       public void onInteraction() {
402           hideInputMethod();
403       }
404    }
405
406    /**
407     * Listens for clicks on the search button.
408     */
409    private class SearchGoButtonClickListener implements View.OnClickListener {
410        public void onClick(View view) {
411            onSearchClicked();
412        }
413    }
414
415    /**
416     * Listens for clicks on the voice search button.
417     */
418    private class VoiceSearchButtonClickListener implements View.OnClickListener {
419        public void onClick(View view) {
420            onVoiceSearchClicked();
421        }
422    }
423
424    /**
425     * Updates the progress bar when the suggestions adapter changes its progress.
426     */
427    private class ProgressUpdater extends DataSetObserver {
428        private final Suggestions mSuggestions;
429
430        public ProgressUpdater(Suggestions suggestions) {
431            mSuggestions = suggestions;
432        }
433
434        @Override
435        public void onChanged() {
436            if (mSuggestions.isDone()) {
437                stopSearchProgress();
438            }
439        }
440    }
441}
442