Merge "Add voice button and a bunch of minor fixes for focus and visibility of ime, submit button and drop down."

This commit is contained in:
Amith Yamasani
2010-10-13 13:19:28 -07:00
committed by Android (Google) Code Review
2 changed files with 174 additions and 14 deletions

View File

@ -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();
}
}
};
@ -741,6 +826,79 @@ public class SearchView extends LinearLayout {
return intent;
}
/**
* 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,