SearchBar.java revision 4fbaf9b54d7f9723f3ec5c320db9cea887eeff7b
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.content.Context;
17import android.content.Intent;
18import android.content.pm.PackageManager;
19import android.content.res.Resources;
20import android.graphics.Color;
21import android.graphics.drawable.Drawable;
22import android.os.Bundle;
23import android.os.Handler;
24import android.os.SystemClock;
25import android.speech.RecognitionListener;
26import android.speech.RecognizerIntent;
27import android.speech.SpeechRecognizer;
28import android.text.Editable;
29import android.text.TextUtils;
30import android.text.TextWatcher;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.LayoutInflater;
34import android.view.ViewGroup;
35import android.view.inputmethod.CompletionInfo;
36import android.view.inputmethod.EditorInfo;
37import android.view.KeyEvent;
38import android.view.MotionEvent;
39import android.view.View;
40import android.widget.ImageView;
41import android.view.inputmethod.InputMethodManager;
42import android.widget.RelativeLayout;
43import android.support.v17.leanback.R;
44import android.widget.TextView;
45
46import java.util.ArrayList;
47import java.util.List;
48
49/**
50 * <p>SearchBar is a search widget.</p>
51 *
52 * <p>Note: Your application will need to request android.permission.RECORD_AUDIO</p>
53 */
54public class SearchBar extends RelativeLayout {
55    private static final String TAG = SearchBar.class.getSimpleName();
56    private static final boolean DEBUG = false;
57
58    private SpeechRecognizer mSpeechRecognizer;
59    private boolean mListening;
60
61    /**
62     * Listener for search query changes
63     */
64    public interface SearchBarListener {
65
66        /**
67         * Method invoked when the search bar detects a change in the query.
68         *
69         * @param query The current full query.
70         */
71        public void onSearchQueryChange(String query);
72
73        /**
74         * <p>Method invoked when the search query is submitted.</p>
75         *
76         * <p>This method can be called without a preceeding onSearchQueryChange,
77         * in particular in the case of a voice input.</p>
78         *
79         * @param query The query being submitted.
80         */
81        public void onSearchQuerySubmit(String query);
82
83        /**
84         * Method invoked when the IME is being dismissed.
85         *
86         * @param query The query set in the search bar at the time the IME is being dismissed.
87         */
88        public void onKeyboardDismiss(String query);
89    }
90
91    private SearchBarListener mSearchBarListener;
92    private SearchEditText mSearchTextEditor;
93    private SpeechOrbView mSpeechOrbView;
94    private ImageView mBadgeView;
95    private String mSearchQuery;
96    private String mTitle;
97    private Drawable mBadgeDrawable;
98    private final Handler mHandler = new Handler();
99    private final InputMethodManager mInputMethodManager;
100    private boolean mAutoStartRecognition = false;
101    private Drawable mBarBackground;
102
103    private int mTextColor;
104    private int mTextSpeechColor;
105    private int mBackgroundAlpha;
106    private int mBackgroundSpeechAlpha;
107    private int mBarHeight;
108
109    public SearchBar(Context context) {
110        this(context, null);
111    }
112
113    public SearchBar(Context context, AttributeSet attrs) {
114        this(context, attrs, 0);
115    }
116
117    public SearchBar(Context context, AttributeSet attrs, int defStyle) {
118        super(context, attrs, defStyle);
119        enforceAudioRecordPermission();
120
121        Resources r = getResources();
122
123        LayoutInflater inflater = LayoutInflater.from(getContext());
124        inflater.inflate(R.layout.lb_search_bar, this, true);
125
126        mBarHeight = getResources().getDimensionPixelSize(R.dimen.lb_search_bar_height);
127        RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
128                mBarHeight);
129        params.addRule(ALIGN_PARENT_TOP, RelativeLayout.TRUE);
130        setLayoutParams(params);
131        setBackgroundColor(Color.TRANSPARENT);
132        setClipChildren(false);
133
134        mSearchQuery = "";
135        mInputMethodManager =
136                (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
137
138        mTextSpeechColor = r.getColor(R.color.lb_search_bar_text_speech_color);
139        mBackgroundSpeechAlpha = r.getInteger(R.integer.lb_search_bar_speech_mode_background_alpha);
140
141        mTextColor = r.getColor(R.color.lb_search_bar_text_color);
142        mBackgroundAlpha = r.getInteger(R.integer.lb_search_bar_text_mode_background_alpha);
143    }
144
145    @Override
146    protected void onFinishInflate() {
147        super.onFinishInflate();
148
149        RelativeLayout items = (RelativeLayout)findViewById(R.id.lb_search_bar_items);
150        mBarBackground = items.getBackground();
151
152        mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor);
153        mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge);
154        if (null != mBadgeDrawable) {
155            mBadgeView.setImageDrawable(mBadgeDrawable);
156        }
157
158        mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
159            @Override
160            public void onFocusChange(View view, boolean hasFocus) {
161                if (DEBUG) Log.v(TAG, "EditText.onFocusChange " + hasFocus);
162                if (hasFocus) {
163                    showNativeKeyboard();
164                }
165                updateUi();
166            }
167        });
168        mSearchTextEditor.addTextChangedListener(new TextWatcher() {
169            @Override
170            public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
171
172            }
173
174            @Override
175            public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
176                if (mSearchTextEditor.hasFocus()) {
177                    setSearchQuery(charSequence.toString());
178                }
179            }
180
181            @Override
182            public void afterTextChanged(Editable editable) {
183
184            }
185        });
186        mSearchTextEditor.setOnKeyboardDismissListener(
187                new SearchEditText.OnKeyboardDismissListener() {
188                    @Override
189                    public void onKeyboardDismiss() {
190                        if (null != mSearchBarListener) {
191                            mSearchBarListener.onKeyboardDismiss(mSearchQuery);
192                        }
193                    }
194                });
195
196        mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() {
197            @Override
198            public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) {
199                if (DEBUG) Log.v(TAG, "onEditorAction: " + action + " event: " + keyEvent);
200                boolean handled = true;
201                if (EditorInfo.IME_ACTION_SEARCH == action && null != mSearchBarListener) {
202                    if (DEBUG) Log.v(TAG, "Action Pressed");
203                    hideNativeKeyboard();
204                    mHandler.postDelayed(new Runnable() {
205                        @Override
206                        public void run() {
207                            if (DEBUG) Log.v(TAG, "Delayed action handling (search)");
208                            submitQuery();
209                        }
210                    }, 500);
211
212                } else if (EditorInfo.IME_ACTION_NONE == action && null != mSearchBarListener) {
213                    if (DEBUG) Log.v(TAG, "Escaped North");
214                    hideNativeKeyboard();
215                    mHandler.postDelayed(new Runnable() {
216                        @Override
217                        public void run() {
218                            if (DEBUG) Log.v(TAG, "Delayed action handling (escape_north)");
219                            mSearchBarListener.onKeyboardDismiss(mSearchQuery);
220                        }
221                    }, 500);
222                } else if (EditorInfo.IME_ACTION_GO == action) {
223                    if (DEBUG) Log.v(TAG, "Voice Clicked");
224                        hideNativeKeyboard();
225                        mHandler.postDelayed(new Runnable() {
226                            @Override
227                            public void run() {
228                                if (DEBUG) Log.v(TAG, "Delayed action handling (voice_mode)");
229                                mAutoStartRecognition = true;
230                                mSpeechOrbView.requestFocus();
231                            }
232                        }, 500);
233                } else {
234                    handled = false;
235                }
236
237                return handled;
238            }
239        });
240
241        mSearchTextEditor.setPrivateImeOptions("EscapeNorth=1;VoiceDismiss=1;");
242
243        mSpeechOrbView = (SpeechOrbView)findViewById(R.id.lb_search_bar_speech_orb);
244        mSpeechOrbView.setOnOrbClickedListener(new OnClickListener() {
245            @Override
246            public void onClick(View view) {
247                startRecognition();
248            }
249        });
250        mSpeechOrbView.setOnFocusChangeListener(new OnFocusChangeListener() {
251            @Override
252            public void onFocusChange(View view, boolean hasFocus) {
253                if (DEBUG) Log.v(TAG, "SpeechOrb.onFocusChange " + hasFocus);
254                if (hasFocus) {
255                    hideNativeKeyboard();
256                    if (mAutoStartRecognition) {
257                        startRecognition();
258                        mAutoStartRecognition = false;
259                    }
260                } else {
261                    stopRecognition();
262                }
263                updateUi();
264            }
265        });
266
267        updateHint();
268        // Start in voice mode
269        mHandler.postDelayed(new Runnable() {
270            @Override
271            public void run() {
272                mAutoStartRecognition = true;
273                mSpeechOrbView.requestFocus();
274            }
275        }, 200);
276    }
277
278    @Override
279    protected void onAttachedToWindow() {
280        super.onAttachedToWindow();
281        mHandler.post(new Runnable() {
282            @Override
283            public void run() {
284                mSearchTextEditor.requestFocus();
285                mSearchTextEditor.requestFocusFromTouch();
286            }
287        });
288    }
289
290    /**
291     * Set a listener for when the term search changes
292     * @param listener
293     */
294    public void setSearchBarListener(SearchBarListener listener) {
295        mSearchBarListener = listener;
296    }
297
298    /**
299     * Set the search query
300     * @param query the search query to use
301     */
302    public void setSearchQuery(String query) {
303        if (query.equals(mSearchQuery)) {
304            return;
305        }
306        mSearchQuery = query;
307        if (null != mSearchBarListener) {
308            mSearchBarListener.onSearchQueryChange(mSearchQuery);
309        }
310    }
311
312    /**
313     * Set the title text used in the hint shown in the search bar.
314     * @param title The hint to use.
315     */
316    public void setTitle(String title) {
317        mTitle = title;
318        updateHint();
319    }
320
321    /**
322     * Returns the current title
323     */
324    public String getTitle() {
325        return mTitle;
326    }
327
328    /**
329     * Set the badge drawable showing inside the search bar.
330     * @param drawable The drawable to be used in the search bar.
331     */
332    public void setBadgeDrawable(Drawable drawable) {
333        mBadgeDrawable = drawable;
334        if (null != mBadgeView) {
335            mBadgeView.setImageDrawable(drawable);
336            if (null != drawable) {
337                mBadgeView.setVisibility(View.VISIBLE);
338            } else {
339                mBadgeView.setVisibility(View.GONE);
340            }
341        }
342    }
343
344    /**
345     * Returns the badge drawable
346     */
347    public Drawable getBadgeDrawable() {
348        return mBadgeDrawable;
349    }
350
351    /**
352     * Update the completion list shown by the IME
353     *
354     * @param completions list of completions shown in the IME, can be null or empty to clear them
355     */
356    public void displayCompletions(List<String> completions) {
357        List<CompletionInfo> infos = new ArrayList<CompletionInfo>();
358        if (null != completions) {
359            for (String completion : completions) {
360                infos.add(new CompletionInfo(infos.size(), infos.size(), completion));
361            }
362        }
363
364        mInputMethodManager.displayCompletions(mSearchTextEditor,
365                infos.toArray(new CompletionInfo[] {}));
366    }
367
368    /**
369     * Set the speech recognizer to be used when doing voice search. The Activity/Fragment is in
370     * charge of creating and destroying the recognizer with its own lifecycle.
371     *
372     * @param recognizer a SpeechRecognizer
373     */
374    public void setSpeechRecognizer(SpeechRecognizer recognizer) {
375        if (null != mSpeechRecognizer) {
376            mSpeechRecognizer.setRecognitionListener(null);
377            if (mListening) {
378                mSpeechRecognizer.stopListening();
379                mListening = false;
380            }
381        }
382        mSpeechRecognizer = recognizer;
383    }
384
385    private void hideNativeKeyboard() {
386        mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(),
387                InputMethodManager.RESULT_UNCHANGED_SHOWN);
388    }
389
390    private void showNativeKeyboard() {
391        mHandler.post(new Runnable() {
392            @Override
393            public void run() {
394                mSearchTextEditor.requestFocusFromTouch();
395                mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
396                        SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN,
397                        mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
398                mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
399                        SystemClock.uptimeMillis(), MotionEvent.ACTION_UP,
400                        mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
401            }
402        });
403    }
404
405    /**
406     * This will update the hint for the search bar properly depending on state and provided title
407     */
408    private void updateHint() {
409        if (null == mSearchTextEditor) return;
410
411        String title = getResources().getString(R.string.lb_search_bar_hint);
412        if (!TextUtils.isEmpty(mTitle)) {
413            if (isVoiceMode()) {
414                title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle);
415            } else {
416                title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle);
417            }
418        } else if (isVoiceMode()) {
419            title = getResources().getString(R.string.lb_search_bar_hint_speech);
420        }
421        mSearchTextEditor.setHint(title);
422    }
423
424    private void stopRecognition() {
425        if (null == mSpeechRecognizer) return;
426
427        if (DEBUG) Log.v(TAG, "stopRecognition " + mListening);
428        mSpeechOrbView.showNotListening();
429
430        if (mListening) {
431            mSpeechRecognizer.cancel();
432        }
433    }
434
435    private void startRecognition() {
436        if (null == mSpeechRecognizer) return;
437
438        if (DEBUG) Log.v(TAG, "startRecognition " + mListening);
439
440        mSearchTextEditor.setText("");
441
442        Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
443
444        recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
445                RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
446        recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
447
448        mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
449            @Override
450            public void onReadyForSpeech(Bundle bundle) {
451                if (DEBUG) Log.v(TAG, "onReadyForSpeech");
452            }
453
454            @Override
455            public void onBeginningOfSpeech() {
456                if (DEBUG) Log.v(TAG, "onBeginningOfSpeech");
457                mListening = true;
458            }
459
460            @Override
461            public void onRmsChanged(float rmsdB) {
462                if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB);
463                int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB);
464                mSpeechOrbView.setSoundLevel(level);
465            }
466
467            @Override
468            public void onBufferReceived(byte[] bytes) {
469                if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length);
470            }
471
472            @Override
473            public void onEndOfSpeech() {
474                if (DEBUG) Log.v(TAG, "onEndOfSpeech");
475                mListening = false;
476            }
477
478            @Override
479            public void onError(int error) {
480                if (DEBUG) Log.v(TAG, "onError " + error);
481                switch (error) {
482                    case SpeechRecognizer.ERROR_NO_MATCH:
483                        Log.d(TAG, "recognizer error no match");
484                        break;
485                    case SpeechRecognizer.ERROR_SERVER:
486                        Log.d(TAG, "recognizer error server error");
487                        break;
488                    case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
489                        Log.d(TAG, "recognizer error speech timeout");
490                        break;
491                    case SpeechRecognizer.ERROR_CLIENT:
492                        Log.d(TAG, "recognizer error client error");
493                        break;
494                    default:
495                        Log.d(TAG, "recognizer other error");
496                        break;
497                }
498
499                mSpeechRecognizer.stopListening();
500                mListening = false;
501                mSpeechRecognizer.setRecognitionListener(null);
502                mSpeechOrbView.showNotListening();
503            }
504
505            @Override
506            public void onResults(Bundle bundle) {
507                if (DEBUG) Log.v(TAG, "onResults");
508                final ArrayList<String> matches =
509                        bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
510                if (matches != null) {
511                    Log.v(TAG, "Got results" + matches);
512
513                    mSearchQuery = matches.get(0);
514                    mSearchTextEditor.setText(mSearchQuery);
515                    submitQuery();
516
517                    if (mListening) {
518                        mSpeechRecognizer.stopListening();
519                    }
520                }
521                mSpeechRecognizer.setRecognitionListener(null);
522                mSpeechOrbView.showNotListening();
523            }
524
525            @Override
526            public void onPartialResults(Bundle bundle) {
527
528            }
529
530            @Override
531            public void onEvent(int i, Bundle bundle) {
532
533            }
534        });
535
536        mSpeechOrbView.showListening();
537        mSpeechRecognizer.startListening(recognizerIntent);
538        mListening = true;
539    }
540
541    private void updateUi() {
542        if (DEBUG) Log.v(TAG, String.format("Update UI %s %s",
543                isVoiceMode() ? "Voice" : "Text",
544                hasFocus() ? "Focused" : "Unfocused"));
545        if (isVoiceMode()) {
546            mBarBackground.setAlpha(mBackgroundSpeechAlpha);
547            mSearchTextEditor.setTextColor(mTextSpeechColor);
548        } else {
549            mBarBackground.setAlpha(mBackgroundAlpha);
550            mSearchTextEditor.setTextColor(mTextColor);
551        }
552
553        updateHint();
554    }
555
556    private boolean isVoiceMode() {
557        return mSpeechOrbView.isFocused();
558    }
559
560    private void submitQuery() {
561        if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) {
562            mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
563        }
564    }
565
566    private void enforceAudioRecordPermission() {
567        String permission = "android.permission.RECORD_AUDIO";
568        int res = getContext().checkCallingOrSelfPermission(permission);
569        if (PackageManager.PERMISSION_GRANTED != res) {
570            throw new IllegalStateException("android.premission.RECORD_AUDIO required for search");
571        }
572    }
573
574}
575