diff options
| -rw-r--r-- | core/java/android/widget/SearchView.java | 176 | ||||
| -rw-r--r-- | core/res/res/layout/search_view.xml | 12 |
2 files changed, 174 insertions, 14 deletions
diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index cfd6754cc439..361b27e7e7ee 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -20,15 +20,23 @@ import static android.widget.SuggestionsAdapter.getColumnString; import com.android.internal.R; +import android.app.PendingIntent; import android.app.SearchManager; import android.app.SearchableInfo; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Bundle; +import android.speech.RecognizerIntent; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -66,6 +74,7 @@ public class SearchView extends LinearLayout { private View mSubmitButton; private View mCloseButton; private View mSearchEditFrame; + private View mVoiceButton; private AutoCompleteTextView mQueryTextView; private boolean mSubmitButtonEnabled; private CharSequence mQueryHint; @@ -74,6 +83,10 @@ public class SearchView extends LinearLayout { private SearchableInfo mSearchable; + // For voice searching + private final Intent mVoiceWebSearchIntent; + private final Intent mVoiceAppSearchIntent; + // A weak map of drawables we've gotten from other packages, so we don't load them // more than once. private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = @@ -162,10 +175,13 @@ public class SearchView extends LinearLayout { mSearchEditFrame = findViewById(R.id.search_edit_frame); mSubmitButton = findViewById(R.id.search_go_btn); mCloseButton = findViewById(R.id.search_close_btn); + mVoiceButton = findViewById(R.id.search_voice_btn); mSearchButton.setOnClickListener(mOnClickListener); mCloseButton.setOnClickListener(mOnClickListener); mSubmitButton.setOnClickListener(mOnClickListener); + mVoiceButton.setOnClickListener(mOnClickListener); + mQueryTextView.addTextChangedListener(mTextWatcher); mQueryTextView.setOnEditorActionListener(mOnEditorActionListener); mQueryTextView.setOnItemClickListener(mOnItemClickListener); @@ -184,6 +200,15 @@ public class SearchView extends LinearLayout { setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); a.recycle(); + // Save voice intent for later queries/launching + mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); + + mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + updateViewsVisibility(mIconifiedByDefault); } @@ -206,12 +231,8 @@ public class SearchView extends LinearLayout { /** @hide */ @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { - if (mClearingFocus) return false; - boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect); - if (result && !isIconified()) { - setImeVisibility(true); - } - return result; + if (mClearingFocus || isIconified()) return false; + return mQueryTextView.requestFocus(direction, previouslyFocusedRect); } /** @hide */ @@ -299,6 +320,7 @@ public class SearchView extends LinearLayout { * @param iconified whether the search field should be iconified by default */ public void setIconifiedByDefault(boolean iconified) { + if (mIconifiedByDefault == iconified) return; mIconifiedByDefault = iconified; updateViewsVisibility(iconified); setImeVisibility(!iconified); @@ -349,8 +371,8 @@ public class SearchView extends LinearLayout { * button is not required. */ public void setSubmitButtonEnabled(boolean enabled) { - mSubmitButton.setVisibility(enabled ? VISIBLE : GONE); mSubmitButtonEnabled = enabled; + updateViewsVisibility(isIconified()); } /** @@ -424,6 +446,7 @@ public class SearchView extends LinearLayout { mSearchButton.setVisibility(visCollapsed); mSubmitButton.setVisibility(mSubmitButtonEnabled && hasText ? visExpanded : GONE); mSearchEditFrame.setVisibility(visExpanded); + updateVoiceButton(!hasText); } private void setImeVisibility(boolean visible) { @@ -458,6 +481,8 @@ public class SearchView extends LinearLayout { onCloseClicked(); } else if (v == mSubmitButton) { onSubmitQuery(); + } else if (v == mVoiceButton) { + onVoiceClicked(); } } }; @@ -525,6 +550,34 @@ public class SearchView extends LinearLayout { } } + /** + * Update the visibility of the voice button. There are actually two voice search modes, + * either of which will activate the button. + * @param empty whether the search query text field is empty. If it is, then the other + * criteria apply to make the voice button visible. Otherwise the voice button will not + * be visible - i.e., if the user has typed a query, remove the voice button. + */ + private void updateVoiceButton(boolean empty) { + int visibility = View.GONE; + if (mSearchable != null && mSearchable.getVoiceSearchEnabled() && empty + && !isIconified()) { + Intent testIntent = null; + if (mSearchable.getVoiceSearchLaunchWebSearch()) { + testIntent = mVoiceWebSearchIntent; + } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { + testIntent = mVoiceAppSearchIntent; + } + if (testIntent != null) { + ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, + PackageManager.MATCH_DEFAULT_ONLY); + if (ri != null) { + visibility = View.VISIBLE; + } + } + } + mVoiceButton.setVisibility(visibility); + } + private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { /** @@ -542,8 +595,10 @@ public class SearchView extends LinearLayout { if (isSubmitButtonEnabled()) { mSubmitButton.setVisibility(hasText ? VISIBLE : GONE); } - if (mOnQueryChangeListener != null) + updateVoiceButton(!hasText); + if (mOnQueryChangeListener != null) { mOnQueryChangeListener.onQueryTextChanged(newText.toString()); + } } private void onSubmitQuery() { @@ -555,21 +610,27 @@ public class SearchView extends LinearLayout { launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); setImeVisibility(false); } + dismissSuggestions(); } } } + private void dismissSuggestions() { + mQueryTextView.dismissDropDown(); + } + private void onCloseClicked() { if (mOnCloseListener == null || !mOnCloseListener.onClose()) { CharSequence text = mQueryTextView.getText(); if (TextUtils.isEmpty(text)) { // query field already empty, hide the keyboard and remove focus - mQueryTextView.clearFocus(); + clearFocus(); setImeVisibility(false); } else { mQueryTextView.setText(""); } updateViewsVisibility(mIconifiedByDefault); + if (mIconifiedByDefault) setImeVisibility(false); } } @@ -579,6 +640,29 @@ public class SearchView extends LinearLayout { setImeVisibility(true); } + private void onVoiceClicked() { + // guard against possible race conditions + if (mSearchable == null) { + return; + } + SearchableInfo searchable = mSearchable; + try { + if (searchable.getVoiceSearchLaunchWebSearch()) { + Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, + searchable); + getContext().startActivity(webSearchIntent); + } else if (searchable.getVoiceSearchLaunchRecognizer()) { + Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, + searchable); + getContext().startActivity(appSearchIntent); + } + } catch (ActivityNotFoundException e) { + // Should not happen, since we check the availability of + // voice search before showing the button. But just in case... + Log.w(LOG_TAG, "Could not find voice search activity"); + } + } + private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { /** @@ -590,6 +674,7 @@ public class SearchView extends LinearLayout { if (mOnSuggestionListener == null || !mOnSuggestionListener.onSuggestionClicked(position)) { launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); + dismissSuggestions(); } } }; @@ -742,6 +827,79 @@ public class SearchView extends LinearLayout { } /** + * Create and return an Intent that can launch the voice search activity for web search. + */ + private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { + Intent voiceIntent = new Intent(baseIntent); + ComponentName searchActivity = searchable.getSearchActivity(); + voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null + : searchActivity.flattenToShortString()); + return voiceIntent; + } + + /** + * Create and return an Intent that can launch the voice search activity, perform a specific + * voice transcription, and forward the results to the searchable activity. + * + * @param baseIntent The voice app search intent to start from + * @return A completely-configured intent ready to send to the voice search activity + */ + private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { + ComponentName searchActivity = searchable.getSearchActivity(); + + // create the necessary intent to set up a search-and-forward operation + // in the voice search system. We have to keep the bundle separate, + // because it becomes immutable once it enters the PendingIntent + Intent queryIntent = new Intent(Intent.ACTION_SEARCH); + queryIntent.setComponent(searchActivity); + PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, + PendingIntent.FLAG_ONE_SHOT); + + // Now set up the bundle that will be inserted into the pending intent + // when it's time to do the search. We always build it here (even if empty) + // because the voice search activity will always need to insert "QUERY" into + // it anyway. + Bundle queryExtras = new Bundle(); + + // Now build the intent to launch the voice search. Add all necessary + // extras to launch the voice recognizer, and then all the necessary extras + // to forward the results to the searchable activity + Intent voiceIntent = new Intent(baseIntent); + + // Add all of the configuration options supplied by the searchable's metadata + String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; + String prompt = null; + String language = null; + int maxResults = 1; + + Resources resources = getResources(); + if (searchable.getVoiceLanguageModeId() != 0) { + languageModel = resources.getString(searchable.getVoiceLanguageModeId()); + } + if (searchable.getVoicePromptTextId() != 0) { + prompt = resources.getString(searchable.getVoicePromptTextId()); + } + if (searchable.getVoiceLanguageId() != 0) { + language = resources.getString(searchable.getVoiceLanguageId()); + } + if (searchable.getVoiceMaxResults() != 0) { + maxResults = searchable.getVoiceMaxResults(); + } + voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); + voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); + voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); + voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); + voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null + : searchActivity.flattenToShortString()); + + // Add the values that configure forwarding the results + voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); + voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); + + return voiceIntent; + } + + /** * When a particular suggestion has been selected, perform the various lookups required * to use the suggestion. This includes checking the cursor for suggestion-specific data, * and/or falling back to the XML for defaults; It also creates REST style Uri data when diff --git a/core/res/res/layout/search_view.xml b/core/res/res/layout/search_view.xml index c229b5970574..b60261e4c76f 100644 --- a/core/res/res/layout/search_view.xml +++ b/core/res/res/layout/search_view.xml @@ -23,14 +23,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:focusable="true" - android:descendantFocusability="afterDescendants"> + > <!-- This is actually used for the badge icon *or* the badge label (or neither) --> <TextView android:id="@+id/search_badge" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="center_vertical" android:layout_marginBottom="2dip" android:drawablePadding="0dip" android:textAppearance="?android:attr/textAppearanceSmall" @@ -41,7 +41,7 @@ <ImageView android:id="@+id/search_button" android:layout_height="36dip" - android:layout_width="36dip" + android:layout_width="match_parent" android:layout_marginRight="7dip" android:layout_gravity="center_vertical" android:src="@android:drawable/ic_btn_search" @@ -51,7 +51,7 @@ <LinearLayout android:id="@+id/search_edit_frame" android:layout_width="300dp" - android:layout_height="wrap_content" + android:layout_height="match_parent" android:visibility="gone" android:orientation="horizontal" android:background="?android:attr/editTextBackground"> @@ -70,6 +70,7 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:layout_weight="1" + android:layout_gravity="bottom" android:paddingLeft="8dip" android:paddingRight="6dip" android:drawablePadding="2dip" @@ -86,6 +87,7 @@ android:popupBackground="@android:drawable/search_dropdown_background" /> + <!-- TODO: Use the generic dialog close drawable --> <ImageView android:id="@+id/search_close_btn" android:layout_width="24dp" |