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