/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.support.v17.leanback.widget; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.media.SoundPool; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.speech.RecognitionListener; import android.speech.RecognizerIntent; import android.speech.SpeechRecognizer; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.util.SparseIntArray; import android.view.LayoutInflater; import android.view.ViewGroup; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.view.inputmethod.InputMethodManager; import android.widget.RelativeLayout; import android.widget.TextView; import android.support.v17.leanback.R; import java.util.ArrayList; import java.util.List; /** * A search widget containing a search orb and a text entry view. * *
* Note: When {@link SpeechRecognitionCallback} is not used, i.e. using {@link SpeechRecognizer}, * your application will need to declare android.permission.RECORD_AUDIO in manifest file. * If your application target >= 23 and the device is running >= 23, it needs implement * {@link SearchBarPermissionListener} where requests runtime permission. *
*/ public class SearchBar extends RelativeLayout { private static final String TAG = SearchBar.class.getSimpleName(); private static final boolean DEBUG = false; private static final float FULL_LEFT_VOLUME = 1.0f; private static final float FULL_RIGHT_VOLUME = 1.0f; private static final int DEFAULT_PRIORITY = 1; private static final int DO_NOT_LOOP = 0; private static final float DEFAULT_RATE = 1.0f; /** * Interface for receiving notification of search query changes. */ public interface SearchBarListener { /** * Method invoked when the search bar detects a change in the query. * * @param query The current full query. */ public void onSearchQueryChange(String query); /** *Method invoked when the search query is submitted.
* *This method can be called without a preceeding onSearchQueryChange, * in particular in the case of a voice input.
* * @param query The query being submitted. */ public void onSearchQuerySubmit(String query); /** * Method invoked when the IME is being dismissed. * * @param query The query set in the search bar at the time the IME is being dismissed. */ public void onKeyboardDismiss(String query); } /** * Interface that handles runtime permissions requests. App sets listener on SearchBar via * {@link #setPermissionListener(SearchBarPermissionListener)}. */ public interface SearchBarPermissionListener { /** * Method invoked when SearchBar asks for "android.permission.RECORD_AUDIO" runtime * permission. */ void requestAudioPermission(); } private AudioManager.OnAudioFocusChangeListener mAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { stopRecognition(); } }; private SearchBarListener mSearchBarListener; private SearchEditText mSearchTextEditor; private SpeechOrbView mSpeechOrbView; private ImageView mBadgeView; private String mSearchQuery; private String mHint; private String mTitle; private Drawable mBadgeDrawable; private final Handler mHandler = new Handler(); private final InputMethodManager mInputMethodManager; private boolean mAutoStartRecognition = false; private Drawable mBarBackground; private final int mTextColor; private final int mTextColorSpeechMode; private final int mTextHintColor; private final int mTextHintColorSpeechMode; private int mBackgroundAlpha; private int mBackgroundSpeechAlpha; private int mBarHeight; private SpeechRecognizer mSpeechRecognizer; private SpeechRecognitionCallback mSpeechRecognitionCallback; private boolean mListening; private SoundPool mSoundPool; private SparseIntArray mSoundMap = new SparseIntArray(); private boolean mRecognizing = false; private final Context mContext; private AudioManager mAudioManager; private SearchBarPermissionListener mPermissionListener; public SearchBar(Context context) { this(context, null); } public SearchBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SearchBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext = context; Resources r = getResources(); LayoutInflater inflater = LayoutInflater.from(getContext()); inflater.inflate(R.layout.lb_search_bar, this, true); mBarHeight = getResources().getDimensionPixelSize(R.dimen.lb_search_bar_height); RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mBarHeight); params.addRule(ALIGN_PARENT_TOP, RelativeLayout.TRUE); setLayoutParams(params); setBackgroundColor(Color.TRANSPARENT); setClipChildren(false); mSearchQuery = ""; mInputMethodManager = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); mTextColorSpeechMode = r.getColor(R.color.lb_search_bar_text_speech_mode); mTextColor = r.getColor(R.color.lb_search_bar_text); mBackgroundSpeechAlpha = r.getInteger(R.integer.lb_search_bar_speech_mode_background_alpha); mBackgroundAlpha = r.getInteger(R.integer.lb_search_bar_text_mode_background_alpha); mTextHintColorSpeechMode = r.getColor(R.color.lb_search_bar_hint_speech_mode); mTextHintColor = r.getColor(R.color.lb_search_bar_hint); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } @Override protected void onFinishInflate() { super.onFinishInflate(); RelativeLayout items = (RelativeLayout)findViewById(R.id.lb_search_bar_items); mBarBackground = items.getBackground(); mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor); mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge); if (null != mBadgeDrawable) { mBadgeView.setImageDrawable(mBadgeDrawable); } mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean hasFocus) { if (DEBUG) Log.v(TAG, "EditText.onFocusChange " + hasFocus); if (hasFocus) { showNativeKeyboard(); } updateUi(hasFocus); } }); final Runnable mOnTextChangedRunnable = new Runnable() { @Override public void run() { setSearchQueryInternal(mSearchTextEditor.getText().toString()); } }; mSearchTextEditor.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { // don't propagate event during speech recognition. if (mRecognizing) { return; } // while IME opens, text editor becomes "" then restores to current value mHandler.removeCallbacks(mOnTextChangedRunnable); mHandler.post(mOnTextChangedRunnable); } @Override public void afterTextChanged(Editable editable) { } }); mSearchTextEditor.setOnKeyboardDismissListener( new SearchEditText.OnKeyboardDismissListener() { @Override public void onKeyboardDismiss() { if (null != mSearchBarListener) { mSearchBarListener.onKeyboardDismiss(mSearchQuery); } } }); mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) { if (DEBUG) Log.v(TAG, "onEditorAction: " + action + " event: " + keyEvent); boolean handled = true; if ((EditorInfo.IME_ACTION_SEARCH == action || EditorInfo.IME_NULL == action) && null != mSearchBarListener) { if (DEBUG) Log.v(TAG, "Action or enter pressed"); hideNativeKeyboard(); mHandler.postDelayed(new Runnable() { @Override public void run() { if (DEBUG) Log.v(TAG, "Delayed action handling (search)"); submitQuery(); } }, 500); } else if (EditorInfo.IME_ACTION_NONE == action && null != mSearchBarListener) { if (DEBUG) Log.v(TAG, "Escaped North"); hideNativeKeyboard(); mHandler.postDelayed(new Runnable() { @Override public void run() { if (DEBUG) Log.v(TAG, "Delayed action handling (escape_north)"); mSearchBarListener.onKeyboardDismiss(mSearchQuery); } }, 500); } else if (EditorInfo.IME_ACTION_GO == action) { if (DEBUG) Log.v(TAG, "Voice Clicked"); hideNativeKeyboard(); mHandler.postDelayed(new Runnable() { @Override public void run() { if (DEBUG) Log.v(TAG, "Delayed action handling (voice_mode)"); mAutoStartRecognition = true; mSpeechOrbView.requestFocus(); } }, 500); } else { handled = false; } return handled; } }); mSearchTextEditor.setPrivateImeOptions("EscapeNorth=1;VoiceDismiss=1;"); mSpeechOrbView = (SpeechOrbView)findViewById(R.id.lb_search_bar_speech_orb); mSpeechOrbView.setOnOrbClickedListener(new OnClickListener() { @Override public void onClick(View view) { toggleRecognition(); } }); mSpeechOrbView.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean hasFocus) { if (DEBUG) Log.v(TAG, "SpeechOrb.onFocusChange " + hasFocus); if (hasFocus) { hideNativeKeyboard(); if (mAutoStartRecognition) { startRecognition(); mAutoStartRecognition = false; } } else { stopRecognition(); } updateUi(hasFocus); } }); updateUi(hasFocus()); updateHint(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (DEBUG) Log.v(TAG, "Loading soundPool"); mSoundPool = new SoundPool(2, AudioManager.STREAM_SYSTEM, 0); loadSounds(mContext); } @Override protected void onDetachedFromWindow() { stopRecognition(); if (DEBUG) Log.v(TAG, "Releasing SoundPool"); mSoundPool.release(); super.onDetachedFromWindow(); } /** * Sets a listener for when the term search changes * @param listener */ public void setSearchBarListener(SearchBarListener listener) { mSearchBarListener = listener; } /** * Sets the search query * @param query the search query to use */ public void setSearchQuery(String query) { stopRecognition(); mSearchTextEditor.setText(query); setSearchQueryInternal(query); } private void setSearchQueryInternal(String query) { if (DEBUG) Log.v(TAG, "setSearchQueryInternal " + query); if (TextUtils.equals(mSearchQuery, query)) { return; } mSearchQuery = query; if (null != mSearchBarListener) { mSearchBarListener.onSearchQueryChange(mSearchQuery); } } /** * Sets the title text used in the hint shown in the search bar. * @param title The hint to use. */ public void setTitle(String title) { mTitle = title; updateHint(); } /** * Returns the current title */ public String getTitle() { return mTitle; } /** * Returns the current search bar hint text. */ public CharSequence getHint() { return mHint; } /** * Sets the badge drawable showing inside the search bar. * @param drawable The drawable to be used in the search bar. */ public void setBadgeDrawable(Drawable drawable) { mBadgeDrawable = drawable; if (null != mBadgeView) { mBadgeView.setImageDrawable(drawable); if (null != drawable) { mBadgeView.setVisibility(View.VISIBLE); } else { mBadgeView.setVisibility(View.GONE); } } } /** * Returns the badge drawable */ public Drawable getBadgeDrawable() { return mBadgeDrawable; } /** * Updates the completion list shown by the IME * * @param completions list of completions shown in the IME, can be null or empty to clear them */ public void displayCompletions(List