summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/widget/SearchView.java176
-rw-r--r--core/res/res/layout/search_view.xml12
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"