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