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