SearchActivityView.java revision 2cbf1d099dbd04c612e3cba34a70293705730fa0
1/*
2 * Copyright (C) 2010 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.ui;
18
19import com.android.quicksearchbox.Corpora;
20import com.android.quicksearchbox.Corpus;
21import com.android.quicksearchbox.CorpusResult;
22import com.android.quicksearchbox.Logger;
23import com.android.quicksearchbox.QsbApplication;
24import com.android.quicksearchbox.R;
25import com.android.quicksearchbox.SearchActivity;
26import com.android.quicksearchbox.SuggestionCursor;
27import com.android.quicksearchbox.Suggestions;
28import com.android.quicksearchbox.VoiceSearch;
29import com.android.quicksearchbox.ui.SuggestionsAdapter.SuggestionsAdapterChangeListener;
30
31import android.app.Activity;
32import android.content.Context;
33import android.database.DataSetObserver;
34import android.graphics.drawable.Drawable;
35import android.text.Editable;
36import android.text.TextWatcher;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.view.KeyEvent;
40import android.view.View;
41import android.view.inputmethod.CompletionInfo;
42import android.view.inputmethod.InputMethodManager;
43import android.widget.AbsListView;
44import android.widget.ImageButton;
45import android.widget.RelativeLayout;
46
47import java.util.ArrayList;
48import java.util.Arrays;
49
50/**
51 *
52 */
53public abstract class SearchActivityView extends RelativeLayout
54        implements SuggestionsAdapterChangeListener {
55    protected static final boolean DBG = false;
56    protected static final String TAG = "QSB.SearchActivityView";
57
58    // The string used for privateImeOptions to identify to the IME that it should not show
59    // a microphone button since one already exists in the search dialog.
60    // TODO: This should move to android-common or something.
61    private static final String IME_OPTION_NO_MICROPHONE = "nm";
62
63    protected QueryTextView mQueryTextView;
64    // True if the query was empty on the previous call to updateQuery()
65    protected boolean mQueryWasEmpty = true;
66    protected Drawable mQueryTextEmptyBg;
67    protected Drawable mQueryTextNotEmptyBg;
68
69    protected SuggestionsView mSuggestionsView;
70
71    protected SuggestionsAdapter mSuggestionsAdapter;
72
73    protected ImageButton mSearchCloseButton;
74    protected ImageButton mSearchGoButton;
75    protected ImageButton mVoiceSearchButton;
76
77    protected ButtonsKeyListener mButtonsKeyListener;
78
79    private boolean mUpdateSuggestions;
80
81    private QueryListener mQueryListener;
82    private SearchClickListener mSearchClickListener;
83
84    public SearchActivityView(Context context) {
85        super(context);
86    }
87
88    public SearchActivityView(Context context, AttributeSet attrs) {
89        super(context, attrs);
90    }
91
92    public SearchActivityView(Context context, AttributeSet attrs, int defStyle) {
93        super(context, attrs, defStyle);
94    }
95
96    @Override
97    protected void onFinishInflate() {
98        mQueryTextView = (QueryTextView) findViewById(R.id.search_src_text);
99
100        mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
101        mSuggestionsView.setOnScrollListener(new InputMethodCloser());
102        mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
103        mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener());
104
105        mSuggestionsAdapter = createSuggestionsAdapter();
106        mSuggestionsAdapter.setSuggestionAdapterChangeListener(this);
107        // TODO: why do we need focus listeners both on the SuggestionsView and the individual
108        // suggestions?
109        mSuggestionsAdapter.setOnFocusChangeListener(new SuggestListFocusListener());
110
111        mSearchCloseButton = (ImageButton) findViewById(R.id.search_close_btn);
112        mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
113        mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
114
115        mQueryTextView.addTextChangedListener(new SearchTextWatcher());
116        mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
117        mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
118        mQueryTextEmptyBg = mQueryTextView.getBackground();
119
120        mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
121
122        mButtonsKeyListener = new ButtonsKeyListener();
123        mSearchGoButton.setOnKeyListener(mButtonsKeyListener);
124        mVoiceSearchButton.setOnKeyListener(mButtonsKeyListener);
125        if (mSearchCloseButton != null) {
126            mSearchCloseButton.setOnKeyListener(mButtonsKeyListener);
127            mSearchCloseButton.setOnClickListener(new CloseClickListener());
128        }
129
130        mUpdateSuggestions = true;
131    }
132
133    public void onSuggestionAdapterChanged() {
134        mSuggestionsView.setAdapter(mSuggestionsAdapter);
135    }
136
137    public abstract void onResume();
138
139    public abstract void onStop();
140
141    public void start() {
142        mSuggestionsAdapter.registerDataSetObserver(new SuggestionsObserver());
143        mSuggestionsView.setAdapter(mSuggestionsAdapter);
144    }
145
146    public void destroy() {
147        mSuggestionsAdapter.setSuggestionAdapterChangeListener(null);
148        mSuggestionsView.setAdapter(null);  // closes mSuggestionsAdapter
149    }
150
151    public void registerSuggestionsObserver(DataSetObserver observer) {
152        mSuggestionsAdapter.registerDataSetObserver(observer);
153    }
154
155    public void unregisterSuggestionsObserver(DataSetObserver observer) {
156        mSuggestionsAdapter.unregisterDataSetObserver(observer);
157    }
158
159    // TODO: Get rid of this. To make it more easily testable,
160    // the SearchActivityView should not depend on QsbApplication.
161    protected QsbApplication getQsbApplication() {
162        return QsbApplication.get(getContext());
163    }
164
165    protected CorpusViewFactory getCorpusViewFactory() {
166        return getQsbApplication().getCorpusViewFactory();
167    }
168
169    private VoiceSearch getVoiceSearch() {
170        return getQsbApplication().getVoiceSearch();
171    }
172
173    protected SuggestionsAdapter createSuggestionsAdapter() {
174        return new DelayingSuggestionsAdapter(getQsbApplication().getDefaultSuggestionViewFactory(),
175                getQsbApplication().getCorpora());
176    }
177
178
179    protected Corpora getCorpora() {
180        return getQsbApplication().getCorpora();
181    }
182
183    public abstract void setCorpus(Corpus corpus);
184
185    public abstract Corpus getCorpus();
186
187    protected Corpus getCorpus(String sourceName) {
188        if (sourceName == null) return null;
189        Corpus corpus = getCorpora().getCorpus(sourceName);
190        if (corpus == null) {
191            Log.w(TAG, "Unknown corpus " + sourceName);
192            return null;
193        }
194        return corpus;
195    }
196
197    public void setCorpus(String corpusName) {
198        if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")");
199        Corpus corpus = getCorpus(corpusName);
200        setCorpus(corpus);
201    }
202
203    public String getCorpusName() {
204        return getCorpus() == null ? null : getCorpus().getName();
205    }
206
207    public abstract Corpus getSearchCorpus();
208
209    public Corpus getWebCorpus() {
210        Corpus webCorpus = getCorpora().getWebCorpus();
211        if (webCorpus == null) {
212            Log.e(TAG, "No web corpus");
213        }
214        return webCorpus;
215    }
216
217    public void setMaxPromoted(int maxPromoted) {
218        mSuggestionsAdapter.setMaxPromoted(maxPromoted);
219    }
220
221    public void setQueryListener(QueryListener listener) {
222        mQueryListener = listener;
223    }
224
225    public void setSearchClickListener(SearchClickListener listener) {
226        mSearchClickListener = listener;
227    }
228
229    public abstract void showCorpusSelectionDialog();
230
231    public abstract void setSettingsButtonClickListener(View.OnClickListener listener);
232
233    public void setVoiceSearchButtonClickListener(View.OnClickListener listener) {
234        if (mVoiceSearchButton != null) {
235            mVoiceSearchButton.setOnClickListener(listener);
236        }
237    }
238
239    public void setSuggestionClickListener(final SuggestionClickListener listener) {
240        mSuggestionsAdapter.setSuggestionClickListener(listener);
241        mQueryTextView.setCommitCompletionListener(new QueryTextView.CommitCompletionListener() {
242            @Override
243            public void onCommitCompletion(int position) {
244                mSuggestionsAdapter.onSuggestionClicked(position);
245            }
246        });
247    }
248
249    protected SuggestionsAdapter getSuggestionsAdapter() {
250        return mSuggestionsAdapter;
251    }
252
253    public Suggestions getSuggestions() {
254        return mSuggestionsAdapter.getSuggestions();
255    }
256
257    public SuggestionCursor getCurrentSuggestions() {
258        return mSuggestionsAdapter.getCurrentSuggestions();
259    }
260
261    public void setSuggestions(Suggestions suggestions) {
262        suggestions.acquire();
263        mSuggestionsAdapter.setSuggestions(suggestions);
264    }
265
266    public void clearSuggestions() {
267        mSuggestionsAdapter.setSuggestions(null);
268    }
269
270    public String getQuery() {
271        CharSequence q = mQueryTextView.getText();
272        return q == null ? "" : q.toString();
273    }
274
275    /**
276     * Sets the text in the query box. Does not update the suggestions.
277     */
278    public void setQuery(String query, boolean selectAll) {
279        mUpdateSuggestions = false;
280        mQueryTextView.setText(query);
281        mQueryTextView.setTextSelection(selectAll);
282        mUpdateSuggestions = true;
283    }
284
285    protected SearchActivity getActivity() {
286        Context context = getContext();
287        if (context instanceof SearchActivity) {
288            return (SearchActivity) context;
289        } else {
290            return null;
291        }
292    }
293
294    public void focusQueryTextView() {
295        mQueryTextView.requestFocus();
296    }
297
298    protected void updateUi(boolean queryEmpty) {
299        updateQueryTextView(queryEmpty);
300        updateSearchGoButton(queryEmpty);
301        updateVoiceSearchButton(queryEmpty);
302    }
303
304    private void updateQueryTextView(boolean queryEmpty) {
305        if (queryEmpty) {
306            if (isSearchCorpusWeb()) {
307                mQueryTextView.setBackgroundDrawable(mQueryTextEmptyBg);
308                mQueryTextView.setHint(null);
309            } else {
310                if (mQueryTextNotEmptyBg == null) {
311                    mQueryTextNotEmptyBg =
312                            getResources().getDrawable(R.drawable.textfield_search_empty);
313                }
314                mQueryTextView.setBackgroundDrawable(mQueryTextNotEmptyBg);
315                Corpus corpus = getCorpus();
316                mQueryTextView.setHint(corpus == null ? "" : corpus.getHint());
317            }
318        } else {
319            mQueryTextView.setBackgroundResource(R.drawable.textfield_search);
320        }
321    }
322
323    private void updateSearchGoButton(boolean queryEmpty) {
324        if (queryEmpty) {
325            mSearchGoButton.setVisibility(View.GONE);
326        } else {
327            mSearchGoButton.setVisibility(View.VISIBLE);
328        }
329    }
330
331    protected void updateVoiceSearchButton(boolean queryEmpty) {
332        if (queryEmpty && getVoiceSearch().shouldShowVoiceSearch(getCorpus())) {
333            mVoiceSearchButton.setVisibility(View.VISIBLE);
334            mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
335        } else {
336            mVoiceSearchButton.setVisibility(View.GONE);
337            mQueryTextView.setPrivateImeOptions(null);
338        }
339    }
340
341    /**
342     * Hides the input method.
343     */
344    protected void hideInputMethod() {
345        InputMethodManager imm = (InputMethodManager)
346                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
347        if (imm != null) {
348            imm.hideSoftInputFromWindow(getWindowToken(), 0);
349        }
350    }
351
352    protected abstract void considerHidingInputMethod();
353
354    public void showInputMethodForQuery() {
355        mQueryTextView.showInputMethod();
356    }
357
358    /**
359     * Overrides the handling of the back key to dismiss the activity.
360     */
361    @Override
362    public boolean dispatchKeyEventPreIme(KeyEvent event) {
363        Activity activity = getActivity();
364        if (activity != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
365            KeyEvent.DispatcherState state = getKeyDispatcherState();
366            if (state != null) {
367                if (event.getAction() == KeyEvent.ACTION_DOWN
368                        && event.getRepeatCount() == 0) {
369                    state.startTracking(event, this);
370                    return true;
371                } else if (event.getAction() == KeyEvent.ACTION_UP
372                        && !event.isCanceled() && state.isTracking(event)) {
373                    hideInputMethod();
374                    activity.onBackPressed();
375                    return true;
376                }
377            }
378        }
379        return super.dispatchKeyEventPreIme(event);
380    }
381
382    /**
383     * If the input method is in fullscreen mode, and the selector corpus
384     * is All or Web, use the web search suggestions as completions.
385     */
386    protected void updateInputMethodSuggestions() {
387        InputMethodManager imm = (InputMethodManager)
388                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
389        if (imm == null || !imm.isFullscreenMode()) return;
390        Suggestions suggestions = mSuggestionsAdapter.getSuggestions();
391        if (suggestions == null) return;
392        CompletionInfo[] completions = webSuggestionsToCompletions(suggestions);
393        if (DBG) Log.d(TAG, "displayCompletions(" + Arrays.toString(completions) + ")");
394        imm.displayCompletions(mQueryTextView, completions);
395    }
396
397    private CompletionInfo[] webSuggestionsToCompletions(Suggestions suggestions) {
398        // TODO: This should also include include web search shortcuts
399        CorpusResult cursor = suggestions.getWebResult();
400        if (cursor == null) return null;
401        int count = cursor.getCount();
402        ArrayList<CompletionInfo> completions = new ArrayList<CompletionInfo>(count);
403        boolean usingWebCorpus = isSearchCorpusWeb();
404        for (int i = 0; i < count; i++) {
405            cursor.moveTo(i);
406            if (!usingWebCorpus || cursor.isWebSearchSuggestion()) {
407                String text1 = cursor.getSuggestionText1();
408                completions.add(new CompletionInfo(i, i, text1));
409            }
410        }
411        return completions.toArray(new CompletionInfo[completions.size()]);
412    }
413
414    /**
415     * Checks if the corpus used for typed searches is the web corpus.
416     */
417    protected boolean isSearchCorpusWeb() {
418        Corpus corpus = getSearchCorpus();
419        return corpus != null && corpus.isWebCorpus();
420    }
421
422    protected boolean onSuggestionKeyDown(SuggestionsAdapter adapter,
423            int position, int keyCode, KeyEvent event) {
424        // Treat enter or search as a click
425        if (       keyCode == KeyEvent.KEYCODE_ENTER
426                || keyCode == KeyEvent.KEYCODE_SEARCH
427                || keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
428            if (adapter != null) {
429                SuggestionsAdapter suggestionsAdapter = adapter;
430                suggestionsAdapter.onSuggestionClicked(position);
431                return true;
432            } else {
433                return false;
434            }
435        }
436
437        return false;
438    }
439
440    protected boolean onSearchClicked(int method) {
441        if (mSearchClickListener != null) {
442            return mSearchClickListener.onSearchClicked(method);
443        }
444        return false;
445    }
446
447    /**
448     * Filters the suggestions list when the search text changes.
449     */
450    private class SearchTextWatcher implements TextWatcher {
451        public void afterTextChanged(Editable s) {
452            boolean empty = s.length() == 0;
453            if (empty != mQueryWasEmpty) {
454                mQueryWasEmpty = empty;
455                updateUi(empty);
456            }
457            if (mUpdateSuggestions) {
458                if (mQueryListener != null) {
459                    mQueryListener.onQueryChanged();
460                }
461            }
462        }
463
464        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
465        }
466
467        public void onTextChanged(CharSequence s, int start, int before, int count) {
468        }
469    }
470
471    /**
472     * Handles key events on the suggestions list view.
473     */
474    protected class SuggestionsViewKeyListener implements View.OnKeyListener {
475        public boolean onKey(View v, int keyCode, KeyEvent event) {
476            if (event.getAction() == KeyEvent.ACTION_DOWN
477                    && v instanceof SuggestionsView) {
478                SuggestionsView view = ((SuggestionsView) v);
479                int position = view.getSelectedPosition();
480                if (onSuggestionKeyDown(view.getAdapter(), position, keyCode, event)) {
481                    return true;
482                }
483            }
484            return forwardKeyToQueryTextView(keyCode, event);
485        }
486    }
487
488    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
489
490        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
491                int totalItemCount) {
492        }
493
494        public void onScrollStateChanged(AbsListView view, int scrollState) {
495            considerHidingInputMethod();
496        }
497    }
498
499    /**
500     * Listens for clicks on the source selector.
501     */
502    private class SearchGoButtonClickListener implements View.OnClickListener {
503        public void onClick(View view) {
504            onSearchClicked(Logger.SEARCH_METHOD_BUTTON);
505        }
506    }
507
508    /**
509     * Handles non-text keys in the query text view.
510     */
511    private class QueryTextViewKeyListener implements View.OnKeyListener {
512        public boolean onKey(View view, int keyCode, KeyEvent event) {
513            // Handle IME search action key
514            if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
515                // if no action was taken, consume the key event so that the keyboard
516                // remains on screen.
517                return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
518            }
519            return false;
520        }
521    }
522
523    /**
524     * Handles key events on the search and voice search buttons,
525     * by refocusing to EditText.
526     */
527    private class ButtonsKeyListener implements View.OnKeyListener {
528        public boolean onKey(View v, int keyCode, KeyEvent event) {
529            return forwardKeyToQueryTextView(keyCode, event);
530        }
531    }
532
533    private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) {
534        if (!event.isSystem() && !isDpadKey(keyCode)) {
535            if (DBG) Log.d(TAG, "Forwarding key to query box: " + event);
536            if (mQueryTextView.requestFocus()) {
537                return mQueryTextView.dispatchKeyEvent(event);
538            }
539        }
540        return false;
541    }
542
543    private boolean isDpadKey(int keyCode) {
544        switch (keyCode) {
545            case KeyEvent.KEYCODE_DPAD_UP:
546            case KeyEvent.KEYCODE_DPAD_DOWN:
547            case KeyEvent.KEYCODE_DPAD_LEFT:
548            case KeyEvent.KEYCODE_DPAD_RIGHT:
549            case KeyEvent.KEYCODE_DPAD_CENTER:
550                return true;
551            default:
552                return false;
553        }
554    }
555
556    /**
557     * Hides the input method when the suggestions get focus.
558     */
559    private class SuggestListFocusListener implements OnFocusChangeListener {
560        public void onFocusChange(View v, boolean focused) {
561            if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused);
562            if (focused) {
563                considerHidingInputMethod();
564            }
565        }
566    }
567
568    private class QueryTextViewFocusListener implements OnFocusChangeListener {
569        public void onFocusChange(View v, boolean focused) {
570            if (DBG) Log.d(TAG, "Query focus change, now: " + focused);
571            if (focused) {
572                // The query box got focus, show the input method
573                showInputMethodForQuery();
574            }
575        }
576    }
577
578    private class SuggestionsObserver extends DataSetObserver {
579        @Override
580        public void onChanged() {
581            updateInputMethodSuggestions();
582        }
583    }
584
585    public interface QueryListener {
586        void onQueryChanged();
587    }
588
589    public interface SearchClickListener {
590        boolean onSearchClicked(int method);
591    }
592
593    private class CloseClickListener implements OnClickListener {
594
595        public void onClick(View v) {
596            mQueryTextView.setText("");
597        }
598    }
599}
600