/* * Copyright (C) 2010 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.widget; import static android.widget.SuggestionsAdapter.getColumnString; import android.annotation.Nullable; 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.Configuration; 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.os.Parcel; import android.os.Parcelable; import android.speech.RecognizerIntent; import android.text.Editable; import android.text.InputType; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.TextWatcher; import android.text.style.ImageSpan; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.CollapsibleActionView; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.TouchDelegate; import android.view.View; import android.view.ViewConfiguration; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.TextView.OnEditorActionListener; import com.android.internal.R; import java.util.WeakHashMap; /** * A widget that provides a user interface for the user to enter a search query and submit a request * to a search provider. Shows a list of query suggestions or results, if available, and allows the * user to pick a suggestion or result to launch into. * *
* When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean) * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done. *
** If you want the search field to always be visible, then call setIconifiedByDefault(false). *
* *For information about using {@code SearchView}, read the * Search developer guide.
** This value may be specified as an empty string to prevent any query hint * from being displayed. * * @param hint the hint text to display or {@code null} to clear * @attr ref android.R.styleable#SearchView_queryHint */ public void setQueryHint(@Nullable CharSequence hint) { mQueryHint = hint; updateQueryHint(); } /** * Returns the hint text that will be displayed in the query text field. *
* The displayed query hint is chosen in the following order: *
The default value is true.
* * @param iconified whether the search field should be iconified by default * * @attr ref android.R.styleable#SearchView_iconifiedByDefault */ public void setIconifiedByDefault(boolean iconified) { if (mIconifiedByDefault == iconified) return; mIconifiedByDefault = iconified; updateViewsVisibility(iconified); updateQueryHint(); } /** * Returns the default iconified state of the search field. * @return * * @attr ref android.R.styleable#SearchView_iconifiedByDefault */ public boolean isIconfiedByDefault() { return mIconifiedByDefault; } /** * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is * a temporary state and does not override the default iconified state set by * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then * a false here will only be valid until the user closes the field. And if the default * state is expanded, then a true here will only clear the text field and not close it. * * @param iconify a true value will collapse the SearchView to an icon, while a false will * expand it. */ public void setIconified(boolean iconify) { if (iconify) { onCloseClicked(); } else { onSearchClicked(); } } /** * Returns the current iconified state of the SearchView. * * @return true if the SearchView is currently iconified, false if the search field is * fully visible. */ public boolean isIconified() { return mIconified; } /** * Enables showing a submit button when the query is non-empty. In cases where the SearchView * is being used to filter the contents of the current activity and doesn't launch a separate * results activity, then the submit button should be disabled. * * @param enabled true to show a submit button for submitting queries, false if a submit * button is not required. */ public void setSubmitButtonEnabled(boolean enabled) { mSubmitButtonEnabled = enabled; updateViewsVisibility(isIconified()); } /** * Returns whether the submit button is enabled when necessary or never displayed. * * @return whether the submit button is enabled automatically when necessary */ public boolean isSubmitButtonEnabled() { return mSubmitButtonEnabled; } /** * Specifies if a query refinement button should be displayed alongside each suggestion * or if it should depend on the flags set in the individual items retrieved from the * suggestions provider. Clicking on the query refinement button will replace the text * in the query text field with the text from the suggestion. This flag only takes effect * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)} * and not when using a custom adapter. * * @param enable true if all items should have a query refinement button, false if only * those items that have a query refinement flag set should have the button. * * @see SearchManager#SUGGEST_COLUMN_FLAGS * @see SearchManager#FLAG_QUERY_REFINEMENT */ public void setQueryRefinementEnabled(boolean enable) { mQueryRefinement = enable; if (mSuggestionsAdapter instanceof SuggestionsAdapter) { ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); } } /** * Returns whether query refinement is enabled for all items or only specific ones. * @return true if enabled for all items, false otherwise. */ public boolean isQueryRefinementEnabled() { return mQueryRefinement; } /** * You can set a custom adapter if you wish. Otherwise the default adapter is used to * display the suggestions from the suggestions provider associated with the SearchableInfo. * * @see #setSearchableInfo(SearchableInfo) */ public void setSuggestionsAdapter(CursorAdapter adapter) { mSuggestionsAdapter = adapter; mSearchSrcTextView.setAdapter(mSuggestionsAdapter); } /** * Returns the adapter used for suggestions, if any. * @return the suggestions adapter */ public CursorAdapter getSuggestionsAdapter() { return mSuggestionsAdapter; } /** * Makes the view at most this many pixels wide * * @attr ref android.R.styleable#SearchView_maxWidth */ public void setMaxWidth(int maxpixels) { mMaxWidth = maxpixels; requestLayout(); } /** * Gets the specified maximum width in pixels, if set. Returns zero if * no maximum width was specified. * @return the maximum width of the view * * @attr ref android.R.styleable#SearchView_maxWidth */ public int getMaxWidth() { return mMaxWidth; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Let the standard measurements take effect in iconified state. if (isIconified()) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } int widthMode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); switch (widthMode) { case MeasureSpec.AT_MOST: // If there is an upper limit, don't exceed maximum width (explicit or implicit) if (mMaxWidth > 0) { width = Math.min(mMaxWidth, width); } else { width = Math.min(getPreferredWidth(), width); } break; case MeasureSpec.EXACTLY: // If an exact width is specified, still don't exceed any specified maximum width if (mMaxWidth > 0) { width = Math.min(mMaxWidth, width); } break; case MeasureSpec.UNSPECIFIED: // Use maximum width, if specified, else preferred width width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth(); break; } widthMode = MeasureSpec.EXACTLY; int heightMode = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); switch (heightMode) { case MeasureSpec.AT_MOST: height = Math.min(getPreferredHeight(), height); break; case MeasureSpec.UNSPECIFIED: height = getPreferredHeight(); break; } heightMode = MeasureSpec.EXACTLY; super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), MeasureSpec.makeMeasureSpec(height, heightMode)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { // Expand mSearchSrcTextView touch target to be the height of the parent in order to // allow it to be up to 48dp. getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds); mSearchSrtTextViewBoundsExpanded.set( mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top); if (mTouchDelegate == null) { mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds, mSearchSrcTextView); setTouchDelegate(mTouchDelegate); } else { mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds); } } } private void getChildBoundsWithinSearchView(View view, Rect rect) { view.getLocationInWindow(mTemp); getLocationInWindow(mTemp2); final int top = mTemp[1] - mTemp2[1]; final int left = mTemp[0] - mTemp2[0]; rect.set(left , top, left + view.getWidth(), top + view.getHeight()); } private int getPreferredWidth() { return getContext().getResources() .getDimensionPixelSize(R.dimen.search_view_preferred_width); } private int getPreferredHeight() { return getContext().getResources() .getDimensionPixelSize(R.dimen.search_view_preferred_height); } private void updateViewsVisibility(final boolean collapsed) { mIconified = collapsed; // Visibility of views that are visible when collapsed final int visCollapsed = collapsed ? VISIBLE : GONE; // Is there text in the query final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText()); mSearchButton.setVisibility(visCollapsed); updateSubmitButton(hasText); mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE); final int iconVisibility; if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) { iconVisibility = GONE; } else { iconVisibility = VISIBLE; } mCollapsedIcon.setVisibility(iconVisibility); updateCloseButton(); updateVoiceButton(!hasText); updateSubmitArea(); } private boolean hasVoiceSearch() { if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) { 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); return ri != null; } } return false; } private boolean isSubmitAreaEnabled() { return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified(); } private void updateSubmitButton(boolean hasText) { int visibility = GONE; if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus() && (hasText || !mVoiceButtonEnabled)) { visibility = VISIBLE; } mGoButton.setVisibility(visibility); } private void updateSubmitArea() { int visibility = GONE; if (isSubmitAreaEnabled() && (mGoButton.getVisibility() == VISIBLE || mVoiceButton.getVisibility() == VISIBLE)) { visibility = VISIBLE; } mSubmitArea.setVisibility(visibility); } private void updateCloseButton() { final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText()); // Should we show the close button? It is not shown if there's no focus, // field is not iconified by default and there is no text in it. final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView); mCloseButton.setVisibility(showClose ? VISIBLE : GONE); final Drawable closeButtonImg = mCloseButton.getDrawable(); if (closeButtonImg != null){ closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET); } } private void postUpdateFocusedState() { post(mUpdateDrawableStateRunnable); } private void updateFocusedState() { final boolean focused = mSearchSrcTextView.hasFocus(); final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET; final Drawable searchPlateBg = mSearchPlate.getBackground(); if (searchPlateBg != null) { searchPlateBg.setState(stateSet); } final Drawable submitAreaBg = mSubmitArea.getBackground(); if (submitAreaBg != null) { submitAreaBg.setState(stateSet); } invalidate(); } @Override protected void onDetachedFromWindow() { removeCallbacks(mUpdateDrawableStateRunnable); post(mReleaseCursorRunnable); super.onDetachedFromWindow(); } private void setImeVisibility(final boolean visible) { if (visible) { post(mShowImeRunnable); } else { removeCallbacks(mShowImeRunnable); InputMethodManager imm = getContext().getSystemService(InputMethodManager.class); if (imm != null) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } } } /** * Called by the SuggestionsAdapter * @hide */ /* package */void onQueryRefine(CharSequence queryText) { setQuery(queryText); } private final OnClickListener mOnClickListener = new OnClickListener() { public void onClick(View v) { if (v == mSearchButton) { onSearchClicked(); } else if (v == mCloseButton) { onCloseClicked(); } else if (v == mGoButton) { onSubmitQuery(); } else if (v == mVoiceButton) { onVoiceClicked(); } else if (v == mSearchSrcTextView) { forceSuggestionQuery(); } } }; /** * Handles the key down event for dealing with action keys. * * @param keyCode This is the keycode of the typed key, and is the same value as * found in the KeyEvent parameter. * @param event The complete event record for the typed key * * @return true if the event was handled here, or false if not. */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mSearchable == null) { return false; } // if it's an action specified by the searchable activity, launch the // entered query with the action key SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText() .toString()); return true; } return super.onKeyDown(keyCode, event); } /** * React to the user typing "enter" or other hardwired keys while typing in * the search box. This handles these special keys while the edit box has * focus. */ View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { public boolean onKey(View v, int keyCode, KeyEvent event) { // guard against possible race conditions if (mSearchable == null) { return false; } if (DBG) { Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: " + mSearchSrcTextView.getListSelection()); } // If a suggestion is selected, handle enter, search key, and action keys // as presses on the selected suggestion if (mSearchSrcTextView.isPopupShowing() && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) { return onSuggestionsKey(v, keyCode, event); } // If there is text in the query box, handle enter, and action keys // The search key is handled by the dialog's onKeyDown(). if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) { if (event.getAction() == KeyEvent.ACTION_UP) { if (keyCode == KeyEvent.KEYCODE_ENTER) { v.cancelLongPress(); // Launch as a regular search. launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText() .toString()); return true; } } if (event.getAction() == KeyEvent.ACTION_DOWN) { SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView .getText().toString()); return true; } } } return false; } }; /** * React to the user typing while in the suggestions list. First, check for * action keys. If not handled, try refocusing regular characters into the * EditText. */ private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { // guard against possible race conditions (late arrival after dismiss) if (mSearchable == null) { return false; } if (mSuggestionsAdapter == null) { return false; } if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) { // First, check for enter or search (both of which we'll treat as a // "click") if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_TAB) { int position = mSearchSrcTextView.getListSelection(); return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); } // Next, check for left/right moves, which we use to "return" the // user to the edit view if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { // give "focus" to text editor, with cursor at the beginning if // left key, at end if right key // TODO: Reverse left/right for right-to-left languages, e.g. // Arabic int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView .length(); mSearchSrcTextView.setSelection(selPoint); mSearchSrcTextView.setListSelection(0); mSearchSrcTextView.clearListSelection(); mSearchSrcTextView.ensureImeVisible(true); return true; } // Next, check for an "up and out" move if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) { // TODO: restoreUserQuery(); // let ACTV complete the move return false; } // Next, check for an "action key" SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); if ((actionKey != null) && ((actionKey.getSuggestActionMsg() != null) || (actionKey .getSuggestActionMsgColumn() != null))) { // launch suggestion using action key column int position = mSearchSrcTextView.getListSelection(); if (position != ListView.INVALID_POSITION) { Cursor c = mSuggestionsAdapter.getCursor(); if (c.moveToPosition(position)) { final String actionMsg = getActionKeyMessage(c, actionKey); if (actionMsg != null && (actionMsg.length() > 0)) { return onItemClicked(position, keyCode, actionMsg); } } } } } return false; } /** * For a given suggestion and a given cursor row, get the action message. If * not provided by the specific row/column, also check for a single * definition (for the action key). * * @param c The cursor providing suggestions * @param actionKey The actionkey record being examined * * @return Returns a string, or null if no action key message for this * suggestion */ private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) { String result = null; // check first in the cursor data, for a suggestion-specific message final String column = actionKey.getSuggestActionMsgColumn(); if (column != null) { result = SuggestionsAdapter.getColumnString(c, column); } // If the cursor didn't give us a message, see if there's a single // message defined // for the actionkey (for all suggestions) if (result == null) { result = actionKey.getSuggestActionMsg(); } return result; } private CharSequence getDecoratedHint(CharSequence hintText) { // If the field is always expanded or we don't have a search hint icon, // then don't add the search icon to the hint. if (!mIconifiedByDefault || mSearchHintIcon == null) { return hintText; } final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25); mSearchHintIcon.setBounds(0, 0, textSize, textSize); final SpannableStringBuilder ssb = new SpannableStringBuilder(" "); ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(hintText); return ssb; } private void updateQueryHint() { final CharSequence hint = getQueryHint(); mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint)); } /** * Updates the auto-complete text view. */ private void updateSearchAutoComplete() { mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold()); mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions()); int inputType = mSearchable.getInputType(); // We only touch this if the input type is set up for text (which it almost certainly // should be, in the case of search!) if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { // The existence of a suggestions authority is the proxy for "suggestions // are available here" inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; if (mSearchable.getSuggestAuthority() != null) { inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing // auto-completion based on its own semantics, which it will present to the user // as they type. This generally means that the input method should not show its // own candidates, and the spell checker should not be in action. The text editor // supplies its candidates by calling InputMethodManager.displayCompletions(), // which in turn will call InputMethodSession.displayCompletions(). inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; } } mSearchSrcTextView.setInputType(inputType); if (mSuggestionsAdapter != null) { mSuggestionsAdapter.changeCursor(null); } // attach the suggestions adapter, if suggestions are available // The existence of a suggestions authority is the proxy for "suggestions available here" if (mSearchable.getSuggestAuthority() != null) { mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable, mOutsideDrawablesCache); mSearchSrcTextView.setAdapter(mSuggestionsAdapter); ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( mQueryRefinement ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); } } /** * 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. */ private void updateVoiceButton(boolean empty) { int visibility = GONE; if (mVoiceButtonEnabled && !isIconified() && empty) { visibility = VISIBLE; mGoButton.setVisibility(GONE); } mVoiceButton.setVisibility(visibility); } private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { /** * Called when the input method default action key is pressed. */ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { onSubmitQuery(); return true; } }; private void onTextChanged(CharSequence newText) { CharSequence text = mSearchSrcTextView.getText(); mUserQuery = text; boolean hasText = !TextUtils.isEmpty(text); updateSubmitButton(hasText); updateVoiceButton(!hasText); updateCloseButton(); updateSubmitArea(); if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) { mOnQueryChangeListener.onQueryTextChange(newText.toString()); } mOldQueryText = newText.toString(); } private void onSubmitQuery() { CharSequence query = mSearchSrcTextView.getText(); if (query != null && TextUtils.getTrimmedLength(query) > 0) { if (mOnQueryChangeListener == null || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { if (mSearchable != null) { launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); } setImeVisibility(false); dismissSuggestions(); } } } private void dismissSuggestions() { mSearchSrcTextView.dismissDropDown(); } private void onCloseClicked() { CharSequence text = mSearchSrcTextView.getText(); if (TextUtils.isEmpty(text)) { if (mIconifiedByDefault) { // If the app doesn't override the close behavior if (mOnCloseListener == null || !mOnCloseListener.onClose()) { // hide the keyboard and remove focus clearFocus(); // collapse the search field updateViewsVisibility(true); } } } else { mSearchSrcTextView.setText(""); mSearchSrcTextView.requestFocus(); setImeVisibility(true); } } private void onSearchClicked() { updateViewsVisibility(false); mSearchSrcTextView.requestFocus(); setImeVisibility(true); if (mOnSearchClickListener != null) { mOnSearchClickListener.onClick(this); } } 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"); } } void onTextFocusChanged() { updateViewsVisibility(isIconified()); // Delayed update to make sure that the focus has settled down and window focus changes // don't affect it. A synchronous update was not working. postUpdateFocusedState(); if (mSearchSrcTextView.hasFocus()) { forceSuggestionQuery(); } } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); postUpdateFocusedState(); } /** * {@inheritDoc} */ @Override public void onActionViewCollapsed() { setQuery("", false); clearFocus(); updateViewsVisibility(true); mSearchSrcTextView.setImeOptions(mCollapsedImeOptions); mExpandedInActionView = false; } /** * {@inheritDoc} */ @Override public void onActionViewExpanded() { if (mExpandedInActionView) return; mExpandedInActionView = true; mCollapsedImeOptions = mSearchSrcTextView.getImeOptions(); mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN); mSearchSrcTextView.setText(""); setIconified(false); } static class SavedState extends BaseSavedState { boolean isIconified; SavedState(Parcelable superState) { super(superState); } public SavedState(Parcel source) { super(source); isIconified = (Boolean) source.readValue(null); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeValue(isIconified); } @Override public String toString() { return "SearchView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " isIconified=" + isIconified + "}"; } public static final Parcelable.Creatornull
if none.
* @return true if a successful launch, false if could not (e.g. bad position).
*/
private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
Cursor c = mSuggestionsAdapter.getCursor();
if ((c != null) && c.moveToPosition(position)) {
Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
// launch the intent
launchIntent(intent);
return true;
}
return false;
}
/**
* Launches an intent, including any special intent handling.
*/
private void launchIntent(Intent intent) {
if (intent == null) {
return;
}
try {
// If the intent was created from a suggestion, it will always have an explicit
// component here.
getContext().startActivity(intent);
} catch (RuntimeException ex) {
Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
}
}
/**
* Sets the text in the query box, without updating the suggestions.
*/
private void setQuery(CharSequence query) {
mSearchSrcTextView.setText(query, true);
// Move the cursor to the end
mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
}
private void launchQuerySearch(int actionKey, String actionMsg, String query) {
String action = Intent.ACTION_SEARCH;
Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
getContext().startActivity(intent);
}
/**
* Constructs an intent from the given information and the search dialog state.
*
* @param action Intent action.
* @param data Intent data, or null
.
* @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or null
.
* @param query Intent query, or null
.
* @param actionKey The key code of the action key that was pressed,
* or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
* @param actionMsg The message for the action key that was pressed,
* or null
if none.
* @param mode The search mode, one of the acceptable values for
* {@link SearchManager#SEARCH_MODE}, or {@code null}.
* @return The intent.
*/
private Intent createIntent(String action, Uri data, String extraData, String query,
int actionKey, String actionMsg) {
// Now build the Intent
Intent intent = new Intent(action);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// We need CLEAR_TOP to avoid reusing an old task that has other activities
// on top of the one we want. We don't want to do this in in-app search though,
// as it can be destructive to the activity stack.
if (data != null) {
intent.setData(data);
}
intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
if (query != null) {
intent.putExtra(SearchManager.QUERY, query);
}
if (extraData != null) {
intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
}
if (mAppSearchData != null) {
intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
}
if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
intent.putExtra(SearchManager.ACTION_KEY, actionKey);
intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
}
intent.setComponent(mSearchable.getSearchActivity());
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();
if (mAppSearchData != null) {
queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
}
// 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
* the suggestion includes a data id.
*
* @param c The suggestions cursor, moved to the row of the user's selection
* @param actionKey The key code of the action key that was pressed,
* or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
* @param actionMsg The message for the action key that was pressed,
* or null
if none.
* @return An intent for the suggestion at the cursor's position.
*/
private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
try {
// use specific action if supplied, or default action if supplied, or fixed default
String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
if (action == null) {
action = mSearchable.getSuggestIntentAction();
}
if (action == null) {
action = Intent.ACTION_SEARCH;
}
// use specific data if supplied, or default data if supplied
String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
if (data == null) {
data = mSearchable.getSuggestIntentData();
}
// then, if an ID was provided, append it.
if (data != null) {
String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
if (id != null) {
data = data + "/" + Uri.encode(id);
}
}
Uri dataUri = (data == null) ? null : Uri.parse(data);
String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
} catch (RuntimeException e ) {
int rowNum;
try { // be really paranoid now
rowNum = c.getPosition();
} catch (RuntimeException e2 ) {
rowNum = -1;
}
Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
" returned exception.", e);
return null;
}
}
private void forceSuggestionQuery() {
mSearchSrcTextView.doBeforeTextChanged();
mSearchSrcTextView.doAfterTextChanged();
}
static boolean isLandscapeMode(Context context) {
return context.getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE;
}
/**
* Callback to watch the text field for empty/non-empty
*/
private TextWatcher mTextWatcher = new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
public void onTextChanged(CharSequence s, int start,
int before, int after) {
SearchView.this.onTextChanged(s);
}
public void afterTextChanged(Editable s) {
}
};
private static class UpdatableTouchDelegate extends TouchDelegate {
/**
* View that should receive forwarded touch events
*/
private final View mDelegateView;
/**
* Bounds in local coordinates of the containing view that should be mapped to the delegate
* view. This rect is used for initial hit testing.
*/
private final Rect mTargetBounds;
/**
* Bounds in local coordinates of the containing view that are actual bounds of the delegate
* view. This rect is used for event coordinate mapping.
*/
private final Rect mActualBounds;
/**
* mTargetBounds inflated to include some slop. This rect is to track whether the motion events
* should be considered to be be within the delegate view.
*/
private final Rect mSlopBounds;
private final int mSlop;
/**
* True if the delegate had been targeted on a down event (intersected mTargetBounds).
*/
private boolean mDelegateTargeted;
public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
super(targetBounds, delegateView);
mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
mTargetBounds = new Rect();
mSlopBounds = new Rect();
mActualBounds = new Rect();
setBounds(targetBounds, actualBounds);
mDelegateView = delegateView;
}
public void setBounds(Rect desiredBounds, Rect actualBounds) {
mTargetBounds.set(desiredBounds);
mSlopBounds.set(desiredBounds);
mSlopBounds.inset(-mSlop, -mSlop);
mActualBounds.set(actualBounds);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
boolean sendToDelegate = false;
boolean hit = true;
boolean handled = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mTargetBounds.contains(x, y)) {
mDelegateTargeted = true;
sendToDelegate = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_MOVE:
sendToDelegate = mDelegateTargeted;
if (sendToDelegate) {
if (!mSlopBounds.contains(x, y)) {
hit = false;
}
}
break;
case MotionEvent.ACTION_CANCEL:
sendToDelegate = mDelegateTargeted;
mDelegateTargeted = false;
break;
}
if (sendToDelegate) {
if (hit && !mActualBounds.contains(x, y)) {
// Offset event coordinates to be in the center of the target view since we
// are within the targetBounds, but not inside the actual bounds of
// mDelegateView
event.setLocation(mDelegateView.getWidth() / 2,
mDelegateView.getHeight() / 2);
} else {
// Offset event coordinates to the target view coordinates.
event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
}
handled = mDelegateView.dispatchTouchEvent(event);
}
return handled;
}
}
/**
* Local subclass for AutoCompleteTextView.
* @hide
*/
public static class SearchAutoComplete extends AutoCompleteTextView {
private int mThreshold;
private SearchView mSearchView;
public SearchAutoComplete(Context context) {
super(context);
mThreshold = getThreshold();
}
public SearchAutoComplete(Context context, AttributeSet attrs) {
super(context, attrs);
mThreshold = getThreshold();
}
public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
super(context, attrs, defStyleAttrs);
mThreshold = getThreshold();
}
public SearchAutoComplete(
Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
super(context, attrs, defStyleAttrs, defStyleRes);
mThreshold = getThreshold();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
DisplayMetrics metrics = getResources().getDisplayMetrics();
setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
getSearchViewTextMinWidthDp(), metrics));
}
void setSearchView(SearchView searchView) {
mSearchView = searchView;
}
@Override
public void setThreshold(int threshold) {
super.setThreshold(threshold);
mThreshold = threshold;
}
/**
* Returns true if the text field is empty, or contains only whitespace.
*/
private boolean isEmpty() {
return TextUtils.getTrimmedLength(getText()) == 0;
}
/**
* We override this method to avoid replacing the query box text when a
* suggestion is clicked.
*/
@Override
protected void replaceText(CharSequence text) {
}
/**
* We override this method to avoid an extra onItemClick being called on
* the drop-down's OnItemClickListener by
* {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
* clicked with the trackball.
*/
@Override
public void performCompletion() {
}
/**
* We override this method to be sure and show the soft keyboard if
* appropriate when the TextView has focus.
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
InputMethodManager inputManager =
getContext().getSystemService(InputMethodManager.class);
inputManager.showSoftInput(this, 0);
// If in landscape mode, then make sure that
// the ime is in front of the dropdown.
if (isLandscapeMode(getContext())) {
ensureImeVisible(true);
}
}
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
mSearchView.onTextFocusChanged();
}
/**
* We override this method so that we can allow a threshold of zero,
* which ACTV does not.
*/
@Override
public boolean enoughToFilter() {
return mThreshold <= 0 || super.enoughToFilter();
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
// special case for the back key, we do not even try to send it
// to the drop down list but instead, consume it immediately
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.handleUpEvent(event);
}
if (event.isTracking() && !event.isCanceled()) {
mSearchView.clearFocus();
mSearchView.setImeVisibility(false);
return true;
}
}
}
return super.onKeyPreIme(keyCode, event);
}
/**
* Get minimum width of the search view text entry area.
*/
private int getSearchViewTextMinWidthDp() {
final Configuration configuration = getResources().getConfiguration();
final int width = configuration.screenWidthDp;
final int height = configuration.screenHeightDp;
final int orientation = configuration.orientation;
if (width >= 960 && height >= 720
&& orientation == Configuration.ORIENTATION_LANDSCAPE) {
return 256;
} else if (width >= 600 || (width >= 640 && height >= 480)) {
return 192;
};
return 160;
}
}
}