SearchBar.java revision 10960072d3c1f9c7f42f9ae77adbfb12f9aed138
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.util.TypedValue;
34import android.view.LayoutInflater;
35import android.view.ViewGroup;
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        mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
138
139
140        mTextSpeechColor = r.getColor(R.color.lb_search_bar_text_speech_color);
141        mBackgroundSpeechAlpha = r.getInteger(R.integer.lb_search_bar_speech_mode_background_alpha);
142
143        mTextColor = r.getColor(R.color.lb_search_bar_text_color);
144        mBackgroundAlpha = r.getInteger(R.integer.lb_search_bar_text_mode_background_alpha);
145    }
146
147    @Override
148    protected void onFinishInflate() {
149        super.onFinishInflate();
150
151        RelativeLayout items = (RelativeLayout)findViewById(R.id.lb_search_bar_items);
152        mBarBackground = items.getBackground();
153
154        mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor);
155        mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge);
156        if (null != mBadgeDrawable) {
157            mBadgeView.setImageDrawable(mBadgeDrawable);
158        }
159
160        mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
161            @Override
162            public void onFocusChange(View view, boolean hasFocus) {
163                if (DEBUG) Log.v(TAG, "EditText.onFocusChange " + hasFocus);
164                if (hasFocus) {
165                    showNativeKeyboard();
166                }
167                updateUi();
168            }
169        });
170        mSearchTextEditor.addTextChangedListener(new TextWatcher() {
171            @Override
172            public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
173
174            }
175
176            @Override
177            public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
178                if (mSearchTextEditor.hasFocus()) {
179                    setSearchQuery(charSequence.toString());
180                }
181            }
182
183            @Override
184            public void afterTextChanged(Editable editable) {
185
186            }
187        });
188        mSearchTextEditor.setOnKeyboardDismissListener(
189                new SearchEditText.OnKeyboardDismissListener() {
190                    @Override
191                    public void onKeyboardDismiss() {
192                        if (null != mSearchBarListener) {
193                            mSearchBarListener.onKeyboardDismiss(mSearchQuery);
194                        }
195                    }
196                });
197
198        mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() {
199            @Override
200            public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) {
201                if (DEBUG) Log.v(TAG, "onEditorAction: " + action + " event: " + keyEvent);
202                boolean handled = true;
203                if (EditorInfo.IME_ACTION_SEARCH == action && null != mSearchBarListener) {
204                    if (DEBUG) Log.v(TAG, "Action Pressed");
205                    hideNativeKeyboard();
206                    mHandler.postDelayed(new Runnable() {
207                        @Override
208                        public void run() {
209                            if (DEBUG) Log.v(TAG, "Delayed action handling (search)");
210                            submitQuery();
211                        }
212                    }, 500);
213
214                } else if (EditorInfo.IME_ACTION_NONE == action && null != mSearchBarListener) {
215                    if (DEBUG) Log.v(TAG, "Escaped North");
216                    hideNativeKeyboard();
217                    mHandler.postDelayed(new Runnable() {
218                        @Override
219                        public void run() {
220                            if (DEBUG) Log.v(TAG, "Delayed action handling (escape_north)");
221                            mSearchBarListener.onKeyboardDismiss(mSearchQuery);
222                        }
223                    }, 500);
224                } else if (EditorInfo.IME_ACTION_GO == action) {
225                    if (DEBUG) Log.v(TAG, "Voice Clicked");
226                        hideNativeKeyboard();
227                        mHandler.postDelayed(new Runnable() {
228                            @Override
229                            public void run() {
230                                if (DEBUG) Log.v(TAG, "Delayed action handling (voice_mode)");
231                                mAutoStartRecognition = true;
232                                mSpeechOrbView.requestFocus();
233                            }
234                        }, 500);
235                } else {
236                    handled = false;
237                }
238
239                return handled;
240            }
241        });
242
243        mSearchTextEditor.setPrivateImeOptions("EscapeNorth=1;VoiceDismiss=1;");
244
245        mSpeechOrbView = (SpeechOrbView)findViewById(R.id.lb_search_bar_speech_orb);
246        mSpeechOrbView.setOnOrbClickedListener(new OnClickListener() {
247            @Override
248            public void onClick(View view) {
249                startRecognition();
250            }
251        });
252        mSpeechOrbView.setOnFocusChangeListener(new OnFocusChangeListener() {
253            @Override
254            public void onFocusChange(View view, boolean hasFocus) {
255                if (DEBUG) Log.v(TAG, "SpeechOrb.onFocusChange " + hasFocus);
256                if (hasFocus) {
257                    hideNativeKeyboard();
258                    if (mAutoStartRecognition) {
259                        startRecognition();
260                        mAutoStartRecognition = false;
261                    }
262                } else {
263                    stopRecognition();
264                }
265                updateUi();
266            }
267        });
268
269        updateHint();
270        // Start in voice mode
271        mHandler.postDelayed(new Runnable() {
272            @Override
273            public void run() {
274                mAutoStartRecognition = true;
275                mSpeechOrbView.requestFocus();
276            }
277        }, 200);
278    }
279
280    @Override
281    protected void onAttachedToWindow() {
282        super.onAttachedToWindow();
283        mHandler.post(new Runnable() {
284            @Override
285            public void run() {
286                mSearchTextEditor.requestFocus();
287                mSearchTextEditor.requestFocusFromTouch();
288            }
289        });
290    }
291
292    /**
293     * Set a listener for when the term search changes
294     * @param listener
295     */
296    public void setSearchBarListener(SearchBarListener listener) {
297        mSearchBarListener = listener;
298    }
299
300    /**
301     * Set the search query
302     * @param query the search query to use
303     */
304    public void setSearchQuery(String query) {
305        if (query.equals(mSearchQuery)) {
306            return;
307        }
308        mSearchQuery = query;
309        if (null != mSearchBarListener) {
310            mSearchBarListener.onSearchQueryChange(mSearchQuery);
311        }
312    }
313
314    /**
315     * Set the title text used in the hint shown in the search bar.
316     * @param title The hint to use.
317     */
318    public void setTitle(String title) {
319        mTitle = title;
320        updateHint();
321    }
322
323    /**
324     * Returns the current title
325     */
326    public String getTitle() {
327        return mTitle;
328    }
329
330    /**
331     * Set the badge drawable showing inside the search bar.
332     * @param drawable The drawable to be used in the search bar.
333     */
334    public void setBadgeDrawable(Drawable drawable) {
335        mBadgeDrawable = drawable;
336        if (null != mBadgeView) {
337            mBadgeView.setImageDrawable(drawable);
338            if (null != drawable) {
339                mBadgeView.setVisibility(View.VISIBLE);
340            } else {
341                mBadgeView.setVisibility(View.GONE);
342            }
343        }
344    }
345
346    /**
347     * Returns the badge drawable
348     */
349    public Drawable getBadgeDrawable() {
350        return mBadgeDrawable;
351    }
352
353    private void hideNativeKeyboard() {
354        mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(),
355                InputMethodManager.RESULT_UNCHANGED_SHOWN);
356    }
357
358    private void showNativeKeyboard() {
359        mHandler.post(new Runnable() {
360            @Override
361            public void run() {
362                mSearchTextEditor.requestFocusFromTouch();
363                mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
364                        SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN,
365                        mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
366                mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
367                        SystemClock.uptimeMillis(), MotionEvent.ACTION_UP,
368                        mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
369            }
370        });
371    }
372
373    /**
374     * This will update the hint for the search bar properly depending on state and provided title
375     */
376    private void updateHint() {
377        if (null == mSearchTextEditor) return;
378
379        String title = getResources().getString(R.string.lb_search_bar_hint);
380        if (!TextUtils.isEmpty(mTitle)) {
381            if (isVoiceMode()) {
382                title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle);
383            } else {
384                title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle);
385            }
386        } else if (isVoiceMode()) {
387            title = getResources().getString(R.string.lb_search_bar_hint_speech);
388        }
389        mSearchTextEditor.setHint(title);
390    }
391
392    private void stopRecognition() {
393        if (DEBUG) Log.v(TAG, "stopRecognition " + mListening);
394        mSpeechOrbView.showNotListening();
395
396        if (mListening) {
397            mSpeechRecognizer.cancel();
398        }
399    }
400
401    private void startRecognition() {
402        if (DEBUG) Log.v(TAG, "startRecognition " + mListening);
403
404        mSearchTextEditor.setText("");
405
406        Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
407
408        recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
409                RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
410        recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
411
412        mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
413            @Override
414            public void onReadyForSpeech(Bundle bundle) {
415                if (DEBUG) Log.v(TAG, "onReadyForSpeech");
416            }
417
418            @Override
419            public void onBeginningOfSpeech() {
420                if (DEBUG) Log.v(TAG, "onBeginningOfSpeech");
421                mListening = true;
422            }
423
424            @Override
425            public void onRmsChanged(float rmsdB) {
426                if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB);
427                int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB);
428                mSpeechOrbView.setSoundLevel(level);
429            }
430
431            @Override
432            public void onBufferReceived(byte[] bytes) {
433                if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length);
434            }
435
436            @Override
437            public void onEndOfSpeech() {
438                if (DEBUG) Log.v(TAG, "onEndOfSpeech");
439                mListening = false;
440            }
441
442            @Override
443            public void onError(int error) {
444                if (DEBUG) Log.v(TAG, "onError " + error);
445                switch (error) {
446                    case SpeechRecognizer.ERROR_NO_MATCH:
447                        Log.d(TAG, "recognizer error no match");
448                        break;
449                    case SpeechRecognizer.ERROR_SERVER:
450                        Log.d(TAG, "recognizer error server error");
451                        break;
452                    case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
453                        Log.d(TAG, "recognizer error speech timeout");
454                        break;
455                    case SpeechRecognizer.ERROR_CLIENT:
456                        Log.d(TAG, "recognizer error client error");
457                        break;
458                    default:
459                        Log.d(TAG, "recognizer other error");
460                        break;
461                }
462
463                mSpeechRecognizer.stopListening();
464                mListening = false;
465                mSpeechRecognizer.setRecognitionListener(null);
466                mSpeechOrbView.showNotListening();
467            }
468
469            @Override
470            public void onResults(Bundle bundle) {
471                if (DEBUG) Log.v(TAG, "onResults");
472                final ArrayList<String> matches =
473                        bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
474                if (matches != null) {
475                    Log.v(TAG, "Got results" + matches);
476
477                    mSearchQuery = matches.get(0);
478                    mSearchTextEditor.setText(mSearchQuery);
479                    submitQuery();
480
481                    if (mListening) {
482                        mSpeechRecognizer.stopListening();
483                    }
484                }
485                mSpeechRecognizer.setRecognitionListener(null);
486                mSpeechOrbView.showNotListening();
487            }
488
489            @Override
490            public void onPartialResults(Bundle bundle) {
491
492            }
493
494            @Override
495            public void onEvent(int i, Bundle bundle) {
496
497            }
498        });
499
500        mSpeechOrbView.showListening();
501        mSpeechRecognizer.startListening(recognizerIntent);
502        mListening = true;
503    }
504
505    private void updateUi() {
506        if (DEBUG) Log.v(TAG, String.format("Update UI %s %s",
507                isVoiceMode() ? "Voice" : "Text",
508                hasFocus() ? "Focused" : "Unfocused"));
509        if (isVoiceMode()) {
510            mBarBackground.setAlpha(mBackgroundSpeechAlpha);
511            mSearchTextEditor.setTextColor(mTextSpeechColor);
512        } else {
513            mBarBackground.setAlpha(mBackgroundAlpha);
514            mSearchTextEditor.setTextColor(mTextColor);
515        }
516
517        updateHint();
518    }
519
520    private boolean isVoiceMode() {
521        return mSpeechOrbView.isFocused();
522    }
523
524    private void submitQuery() {
525        if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) {
526            mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
527        }
528    }
529
530    private void enforceAudioRecordPermission() {
531        String permission = "android.permission.RECORD_AUDIO";
532        int res = getContext().checkCallingOrSelfPermission(permission);
533        if (PackageManager.PERMISSION_GRANTED != res) {
534            throw new IllegalStateException("android.premission.RECORD_AUDIO required for search");
535        }
536    }
537
538}
539