SearchView.java revision 2e21b5e22c320fd7e6af86a7cc05b4b11d7a0f64
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.widget; 18 19import android.app.PendingIntent; 20import android.app.SearchManager; 21import android.app.SearchableInfo; 22import android.content.ActivityNotFoundException; 23import android.content.ComponentName; 24import android.content.Context; 25import android.content.Intent; 26import android.content.pm.PackageManager; 27import android.content.pm.ResolveInfo; 28import android.content.res.Configuration; 29import android.content.res.Resources; 30import android.content.res.TypedArray; 31import android.database.Cursor; 32import android.graphics.Rect; 33import android.graphics.drawable.Drawable; 34import android.net.Uri; 35import android.os.Build; 36import android.os.Bundle; 37import android.os.ResultReceiver; 38import android.speech.RecognizerIntent; 39import android.support.v4.view.KeyEventCompat; 40import android.support.v4.widget.CursorAdapter; 41import android.support.v7.appcompat.R; 42import android.support.v7.view.CollapsibleActionView; 43import android.text.Editable; 44import android.text.InputType; 45import android.text.Spannable; 46import android.text.SpannableStringBuilder; 47import android.text.TextUtils; 48import android.text.TextWatcher; 49import android.text.style.ImageSpan; 50import android.util.AttributeSet; 51import android.util.Log; 52import android.util.TypedValue; 53import android.view.KeyEvent; 54import android.view.LayoutInflater; 55import android.view.View; 56import android.view.ViewTreeObserver; 57import android.view.inputmethod.EditorInfo; 58import android.view.inputmethod.InputMethodManager; 59import android.widget.AdapterView; 60import android.widget.AdapterView.OnItemClickListener; 61import android.widget.AdapterView.OnItemSelectedListener; 62import android.widget.AutoCompleteTextView; 63import android.widget.ImageView; 64import android.widget.LinearLayout; 65import android.widget.ListView; 66import android.widget.TextView; 67import android.widget.TextView.OnEditorActionListener; 68 69import java.lang.reflect.Method; 70import java.util.WeakHashMap; 71 72import static android.support.v7.widget.SuggestionsAdapter.getColumnString; 73 74/** 75 * A widget that provides a user interface for the user to enter a search query and submit a request 76 * to a search provider. Shows a list of query suggestions or results, if available, and allows the 77 * user to pick a suggestion or result to launch into. 78 * 79 * <p> 80 * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it 81 * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean) 82 * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done. 83 * </p> 84 * <p> 85 * If you want the search field to always be visible, then call setIconifiedByDefault(false). 86 * </p> 87 * 88 * <div class="special reference"> 89 * <h3>Developer Guides</h3> 90 * <p>For information about using {@code SearchView}, read the 91 * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p> 92 * </div> 93 * 94 * @see android.support.v4.view.MenuItemCompat#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW 95 */ 96public class SearchView extends LinearLayout implements CollapsibleActionView { 97 98 private static final boolean DBG = false; 99 private static final String LOG_TAG = "SearchView"; 100 101 /** 102 * Private constant for removing the microphone in the keyboard. 103 */ 104 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 105 106 private OnQueryTextListener mOnQueryChangeListener; 107 private OnCloseListener mOnCloseListener; 108 private View.OnFocusChangeListener mOnQueryTextFocusChangeListener; 109 private OnSuggestionListener mOnSuggestionListener; 110 private OnClickListener mOnSearchClickListener; 111 112 private boolean mIconifiedByDefault; 113 private boolean mIconified; 114 private CursorAdapter mSuggestionsAdapter; 115 private View mSearchButton; 116 private View mSubmitButton; 117 private View mSearchPlate; 118 private View mSubmitArea; 119 private ImageView mCloseButton; 120 private View mSearchEditFrame; 121 private View mVoiceButton; 122 private SearchAutoComplete mQueryTextView; 123 private View mDropDownAnchor; 124 private ImageView mSearchHintIcon; 125 private boolean mSubmitButtonEnabled; 126 private CharSequence mQueryHint; 127 private boolean mQueryRefinement; 128 private boolean mClearingFocus; 129 private int mMaxWidth; 130 private boolean mVoiceButtonEnabled; 131 private CharSequence mOldQueryText; 132 private CharSequence mUserQuery; 133 private boolean mExpandedInActionView; 134 private int mCollapsedImeOptions; 135 136 private SearchableInfo mSearchable; 137 private Bundle mAppSearchData; 138 139 static final AutoCompleteTextViewReflector HIDDEN_METHOD_INVOKER = new AutoCompleteTextViewReflector(); 140 141 /* 142 * SearchView can be set expanded before the IME is ready to be shown during 143 * initial UI setup. The show operation is asynchronous to account for this. 144 */ 145 private Runnable mShowImeRunnable = new Runnable() { 146 public void run() { 147 InputMethodManager imm = (InputMethodManager) 148 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 149 150 if (imm != null) { 151 HIDDEN_METHOD_INVOKER.showSoftInputUnchecked(imm, SearchView.this, 0); 152 } 153 } 154 }; 155 156 private Runnable mUpdateDrawableStateRunnable = new Runnable() { 157 public void run() { 158 updateFocusedState(); 159 } 160 }; 161 162 private Runnable mReleaseCursorRunnable = new Runnable() { 163 public void run() { 164 if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) { 165 mSuggestionsAdapter.changeCursor(null); 166 } 167 } 168 }; 169 170 // For voice searching 171 private final Intent mVoiceWebSearchIntent; 172 private final Intent mVoiceAppSearchIntent; 173 174 // A weak map of drawables we've gotten from other packages, so we don't load them 175 // more than once. 176 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = 177 new WeakHashMap<String, Drawable.ConstantState>(); 178 179 /** 180 * Callbacks for changes to the query text. 181 */ 182 public interface OnQueryTextListener { 183 184 /** 185 * Called when the user submits the query. This could be due to a key press on the 186 * keyboard or due to pressing a submit button. 187 * The listener can override the standard behavior by returning true 188 * to indicate that it has handled the submit request. Otherwise return false to 189 * let the SearchView handle the submission by launching any associated intent. 190 * 191 * @param query the query text that is to be submitted 192 * 193 * @return true if the query has been handled by the listener, false to let the 194 * SearchView perform the default action. 195 */ 196 boolean onQueryTextSubmit(String query); 197 198 /** 199 * Called when the query text is changed by the user. 200 * 201 * @param newText the new content of the query text field. 202 * 203 * @return false if the SearchView should perform the default action of showing any 204 * suggestions if available, true if the action was handled by the listener. 205 */ 206 boolean onQueryTextChange(String newText); 207 } 208 209 public interface OnCloseListener { 210 211 /** 212 * The user is attempting to close the SearchView. 213 * 214 * @return true if the listener wants to override the default behavior of clearing the 215 * text field and dismissing it, false otherwise. 216 */ 217 boolean onClose(); 218 } 219 220 /** 221 * Callback interface for selection events on suggestions. These callbacks 222 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}. 223 */ 224 public interface OnSuggestionListener { 225 226 /** 227 * Called when a suggestion was selected by navigating to it. 228 * @param position the absolute position in the list of suggestions. 229 * 230 * @return true if the listener handles the event and wants to override the default 231 * behavior of possibly rewriting the query based on the selected item, false otherwise. 232 */ 233 boolean onSuggestionSelect(int position); 234 235 /** 236 * Called when a suggestion was clicked. 237 * @param position the absolute position of the clicked item in the list of suggestions. 238 * 239 * @return true if the listener handles the event and wants to override the default 240 * behavior of launching any intent or submitting a search query specified on that item. 241 * Return false otherwise. 242 */ 243 boolean onSuggestionClick(int position); 244 } 245 246 public SearchView(Context context) { 247 this(context, null); 248 } 249 250 public SearchView(Context context, AttributeSet attrs) { 251 super(context, attrs); 252 253 LayoutInflater inflater = (LayoutInflater) context 254 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 255 inflater.inflate(R.layout.abc_search_view, this, true); 256 257 mSearchButton = findViewById(R.id.search_button); 258 mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text); 259 mQueryTextView.setSearchView(this); 260 261 mSearchEditFrame = findViewById(R.id.search_edit_frame); 262 mSearchPlate = findViewById(R.id.search_plate); 263 mSubmitArea = findViewById(R.id.submit_area); 264 mSubmitButton = findViewById(R.id.search_go_btn); 265 mCloseButton = (ImageView) findViewById(R.id.search_close_btn); 266 mVoiceButton = findViewById(R.id.search_voice_btn); 267 mSearchHintIcon = (ImageView) findViewById(R.id.search_mag_icon); 268 269 mSearchButton.setOnClickListener(mOnClickListener); 270 mCloseButton.setOnClickListener(mOnClickListener); 271 mSubmitButton.setOnClickListener(mOnClickListener); 272 mVoiceButton.setOnClickListener(mOnClickListener); 273 mQueryTextView.setOnClickListener(mOnClickListener); 274 275 mQueryTextView.addTextChangedListener(mTextWatcher); 276 mQueryTextView.setOnEditorActionListener(mOnEditorActionListener); 277 mQueryTextView.setOnItemClickListener(mOnItemClickListener); 278 mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener); 279 mQueryTextView.setOnKeyListener(mTextKeyListener); 280 // Inform any listener of focus changes 281 mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() { 282 283 public void onFocusChange(View v, boolean hasFocus) { 284 if (mOnQueryTextFocusChangeListener != null) { 285 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus); 286 } 287 } 288 }); 289 290 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0); 291 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); 292 int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_android_maxWidth, -1); 293 if (maxWidth != -1) { 294 setMaxWidth(maxWidth); 295 } 296 CharSequence queryHint = a.getText(R.styleable.SearchView_queryHint); 297 if (!TextUtils.isEmpty(queryHint)) { 298 setQueryHint(queryHint); 299 } 300 int imeOptions = a.getInt(R.styleable.SearchView_android_imeOptions, -1); 301 if (imeOptions != -1) { 302 setImeOptions(imeOptions); 303 } 304 int inputType = a.getInt(R.styleable.SearchView_android_inputType, -1); 305 if (inputType != -1) { 306 setInputType(inputType); 307 } 308 309 a.recycle(); 310 311 boolean focusable = true; 312 313 a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0); 314 focusable = a.getBoolean(R.styleable.View_android_focusable, focusable); 315 a.recycle(); 316 setFocusable(focusable); 317 318 // Save voice intent for later queries/launching 319 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); 320 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 321 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 322 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 323 324 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 325 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 326 327 mDropDownAnchor = findViewById(mQueryTextView.getDropDownAnchor()); 328 if (mDropDownAnchor != null) { 329 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 330 addOnLayoutChangeListenerToDropDownAnchorSDK11(); 331 } else { 332 addOnLayoutChangeListenerToDropDownAnchorBase(); 333 } 334 } 335 336 updateViewsVisibility(mIconifiedByDefault); 337 updateQueryHint(); 338 } 339 340 private void addOnLayoutChangeListenerToDropDownAnchorSDK11() { 341 mDropDownAnchor.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 342 @Override 343 public void onLayoutChange(View v, int left, int top, int right, int bottom, 344 int oldLeft, int oldTop, int oldRight, int oldBottom) { 345 adjustDropDownSizeAndPosition(); 346 } 347 }); 348 } 349 350 private void addOnLayoutChangeListenerToDropDownAnchorBase() { 351 mDropDownAnchor.getViewTreeObserver() 352 .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 353 @Override 354 public void onGlobalLayout() { 355 adjustDropDownSizeAndPosition(); 356 } 357 }); 358 } 359 360 /** 361 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used 362 * to display labels, hints, suggestions, create intents for launching search results screens 363 * and controlling other affordances such as a voice button. 364 * 365 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific 366 * activity or a global search provider. 367 */ 368 public void setSearchableInfo(SearchableInfo searchable) { 369 mSearchable = searchable; 370 if (mSearchable != null) { 371 updateSearchAutoComplete(); 372 updateQueryHint(); 373 } 374 // Cache the voice search capability 375 mVoiceButtonEnabled = hasVoiceSearch(); 376 377 if (mVoiceButtonEnabled) { 378 // Disable the microphone on the keyboard, as a mic is displayed near the text box 379 // TODO: use imeOptions to disable voice input when the new API will be available 380 mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 381 } 382 updateViewsVisibility(isIconified()); 383 } 384 385 /** 386 * Sets the APP_DATA for legacy SearchDialog use. 387 * @param appSearchData bundle provided by the app when launching the search dialog 388 * @hide 389 */ 390 public void setAppSearchData(Bundle appSearchData) { 391 mAppSearchData = appSearchData; 392 } 393 394 /** 395 * Sets the IME options on the query text field. 396 * 397 * @see TextView#setImeOptions(int) 398 * @param imeOptions the options to set on the query text field 399 * 400 * @attr ref android.R.styleable#SearchView_imeOptions 401 */ 402 public void setImeOptions(int imeOptions) { 403 mQueryTextView.setImeOptions(imeOptions); 404 } 405 406 /** 407 * Returns the IME options set on the query text field. 408 * @return the ime options 409 * @see TextView#setImeOptions(int) 410 * 411 * @attr ref android.R.styleable#SearchView_imeOptions 412 */ 413 public int getImeOptions() { 414 return mQueryTextView.getImeOptions(); 415 } 416 417 /** 418 * Sets the input type on the query text field. 419 * 420 * @see TextView#setInputType(int) 421 * @param inputType the input type to set on the query text field 422 * 423 * @attr ref android.R.styleable#SearchView_inputType 424 */ 425 public void setInputType(int inputType) { 426 mQueryTextView.setInputType(inputType); 427 } 428 429 /** 430 * Returns the input type set on the query text field. 431 * @return the input type 432 * 433 * @attr ref android.R.styleable#SearchView_inputType 434 */ 435 public int getInputType() { 436 return mQueryTextView.getInputType(); 437 } 438 439 /** @hide */ 440 @Override 441 public boolean requestFocus(int direction, Rect previouslyFocusedRect) { 442 // Don't accept focus if in the middle of clearing focus 443 if (mClearingFocus) return false; 444 // Check if SearchView is focusable. 445 if (!isFocusable()) return false; 446 // If it is not iconified, then give the focus to the text field 447 if (!isIconified()) { 448 boolean result = mQueryTextView.requestFocus(direction, previouslyFocusedRect); 449 if (result) { 450 updateViewsVisibility(false); 451 } 452 return result; 453 } else { 454 return super.requestFocus(direction, previouslyFocusedRect); 455 } 456 } 457 458 /** @hide */ 459 @Override 460 public void clearFocus() { 461 mClearingFocus = true; 462 setImeVisibility(false); 463 super.clearFocus(); 464 mQueryTextView.clearFocus(); 465 mClearingFocus = false; 466 } 467 468 /** 469 * Sets a listener for user actions within the SearchView. 470 * 471 * @param listener the listener object that receives callbacks when the user performs 472 * actions in the SearchView such as clicking on buttons or typing a query. 473 */ 474 public void setOnQueryTextListener(OnQueryTextListener listener) { 475 mOnQueryChangeListener = listener; 476 } 477 478 /** 479 * Sets a listener to inform when the user closes the SearchView. 480 * 481 * @param listener the listener to call when the user closes the SearchView. 482 */ 483 public void setOnCloseListener(OnCloseListener listener) { 484 mOnCloseListener = listener; 485 } 486 487 /** 488 * Sets a listener to inform when the focus of the query text field changes. 489 * 490 * @param listener the listener to inform of focus changes. 491 */ 492 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { 493 mOnQueryTextFocusChangeListener = listener; 494 } 495 496 /** 497 * Sets a listener to inform when a suggestion is focused or clicked. 498 * 499 * @param listener the listener to inform of suggestion selection events. 500 */ 501 public void setOnSuggestionListener(OnSuggestionListener listener) { 502 mOnSuggestionListener = listener; 503 } 504 505 /** 506 * Sets a listener to inform when the search button is pressed. This is only 507 * relevant when the text field is not visible by default. Calling {@link #setIconified 508 * setIconified(false)} can also cause this listener to be informed. 509 * 510 * @param listener the listener to inform when the search button is clicked or 511 * the text field is programmatically de-iconified. 512 */ 513 public void setOnSearchClickListener(OnClickListener listener) { 514 mOnSearchClickListener = listener; 515 } 516 517 /** 518 * Returns the query string currently in the text field. 519 * 520 * @return the query string 521 */ 522 public CharSequence getQuery() { 523 return mQueryTextView.getText(); 524 } 525 526 /** 527 * Sets a query string in the text field and optionally submits the query as well. 528 * 529 * @param query the query string. This replaces any query text already present in the 530 * text field. 531 * @param submit whether to submit the query right now or only update the contents of 532 * text field. 533 */ 534 public void setQuery(CharSequence query, boolean submit) { 535 mQueryTextView.setText(query); 536 if (query != null) { 537 mQueryTextView.setSelection(mQueryTextView.length()); 538 mUserQuery = query; 539 } 540 541 // If the query is not empty and submit is requested, submit the query 542 if (submit && !TextUtils.isEmpty(query)) { 543 onSubmitQuery(); 544 } 545 } 546 547 /** 548 * Sets the hint text to display in the query text field. This overrides any hint specified 549 * in the SearchableInfo. 550 * 551 * @param hint the hint text to display 552 * 553 * @attr ref android.R.styleable#SearchView_queryHint 554 */ 555 public void setQueryHint(CharSequence hint) { 556 mQueryHint = hint; 557 updateQueryHint(); 558 } 559 560 /** 561 * Gets the hint text to display in the query text field. 562 * @return the query hint text, if specified, null otherwise. 563 * 564 * @attr ref android.R.styleable#SearchView_queryHint 565 */ 566 public CharSequence getQueryHint() { 567 if (mQueryHint != null) { 568 return mQueryHint; 569 } else if (mSearchable != null) { 570 CharSequence hint = null; 571 int hintId = mSearchable.getHintId(); 572 if (hintId != 0) { 573 hint = getContext().getString(hintId); 574 } 575 return hint; 576 } 577 return null; 578 } 579 580 /** 581 * Sets the default or resting state of the search field. If true, a single search icon is 582 * shown by default and expands to show the text field and other buttons when pressed. Also, 583 * if the default state is iconified, then it collapses to that state when the close button 584 * is pressed. Changes to this property will take effect immediately. 585 * 586 * <p>The default value is true.</p> 587 * 588 * @param iconified whether the search field should be iconified by default 589 * 590 * @attr ref android.R.styleable#SearchView_iconifiedByDefault 591 */ 592 public void setIconifiedByDefault(boolean iconified) { 593 if (mIconifiedByDefault == iconified) return; 594 mIconifiedByDefault = iconified; 595 updateViewsVisibility(iconified); 596 updateQueryHint(); 597 } 598 599 /** 600 * Returns the default iconified state of the search field. 601 * @return 602 * 603 * @attr ref android.R.styleable#SearchView_iconifiedByDefault 604 */ 605 public boolean isIconfiedByDefault() { 606 return mIconifiedByDefault; 607 } 608 609 /** 610 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is 611 * a temporary state and does not override the default iconified state set by 612 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then 613 * a false here will only be valid until the user closes the field. And if the default 614 * state is expanded, then a true here will only clear the text field and not close it. 615 * 616 * @param iconify a true value will collapse the SearchView to an icon, while a false will 617 * expand it. 618 */ 619 public void setIconified(boolean iconify) { 620 if (iconify) { 621 onCloseClicked(); 622 } else { 623 onSearchClicked(); 624 } 625 } 626 627 /** 628 * Returns the current iconified state of the SearchView. 629 * 630 * @return true if the SearchView is currently iconified, false if the search field is 631 * fully visible. 632 */ 633 public boolean isIconified() { 634 return mIconified; 635 } 636 637 /** 638 * Enables showing a submit button when the query is non-empty. In cases where the SearchView 639 * is being used to filter the contents of the current activity and doesn't launch a separate 640 * results activity, then the submit button should be disabled. 641 * 642 * @param enabled true to show a submit button for submitting queries, false if a submit 643 * button is not required. 644 */ 645 public void setSubmitButtonEnabled(boolean enabled) { 646 mSubmitButtonEnabled = enabled; 647 updateViewsVisibility(isIconified()); 648 } 649 650 /** 651 * Returns whether the submit button is enabled when necessary or never displayed. 652 * 653 * @return whether the submit button is enabled automatically when necessary 654 */ 655 public boolean isSubmitButtonEnabled() { 656 return mSubmitButtonEnabled; 657 } 658 659 /** 660 * Specifies if a query refinement button should be displayed alongside each suggestion 661 * or if it should depend on the flags set in the individual items retrieved from the 662 * suggestions provider. Clicking on the query refinement button will replace the text 663 * in the query text field with the text from the suggestion. This flag only takes effect 664 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)} 665 * and not when using a custom adapter. 666 * 667 * @param enable true if all items should have a query refinement button, false if only 668 * those items that have a query refinement flag set should have the button. 669 * 670 * @see SearchManager#SUGGEST_COLUMN_FLAGS 671 * @see SearchManager#FLAG_QUERY_REFINEMENT 672 */ 673 public void setQueryRefinementEnabled(boolean enable) { 674 mQueryRefinement = enable; 675 if (mSuggestionsAdapter instanceof SuggestionsAdapter) { 676 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( 677 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); 678 } 679 } 680 681 /** 682 * Returns whether query refinement is enabled for all items or only specific ones. 683 * @return true if enabled for all items, false otherwise. 684 */ 685 public boolean isQueryRefinementEnabled() { 686 return mQueryRefinement; 687 } 688 689 /** 690 * You can set a custom adapter if you wish. Otherwise the default adapter is used to 691 * display the suggestions from the suggestions provider associated with the SearchableInfo. 692 * 693 * @see #setSearchableInfo(SearchableInfo) 694 */ 695 public void setSuggestionsAdapter(CursorAdapter adapter) { 696 mSuggestionsAdapter = adapter; 697 698 mQueryTextView.setAdapter(mSuggestionsAdapter); 699 } 700 701 /** 702 * Returns the adapter used for suggestions, if any. 703 * @return the suggestions adapter 704 */ 705 public CursorAdapter getSuggestionsAdapter() { 706 return mSuggestionsAdapter; 707 } 708 709 /** 710 * Makes the view at most this many pixels wide 711 * 712 * @attr ref android.R.styleable#SearchView_maxWidth 713 */ 714 public void setMaxWidth(int maxpixels) { 715 mMaxWidth = maxpixels; 716 717 requestLayout(); 718 } 719 720 /** 721 * Gets the specified maximum width in pixels, if set. Returns zero if 722 * no maximum width was specified. 723 * @return the maximum width of the view 724 * 725 * @attr ref android.R.styleable#SearchView_maxWidth 726 */ 727 public int getMaxWidth() { 728 return mMaxWidth; 729 } 730 731 @Override 732 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 733 // Let the standard measurements take effect in iconified state. 734 if (isIconified()) { 735 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 736 return; 737 } 738 739 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 740 int width = MeasureSpec.getSize(widthMeasureSpec); 741 742 switch (widthMode) { 743 case MeasureSpec.AT_MOST: 744 // If there is an upper limit, don't exceed maximum width (explicit or implicit) 745 if (mMaxWidth > 0) { 746 width = Math.min(mMaxWidth, width); 747 } else { 748 width = Math.min(getPreferredWidth(), width); 749 } 750 break; 751 case MeasureSpec.EXACTLY: 752 // If an exact width is specified, still don't exceed any specified maximum width 753 if (mMaxWidth > 0) { 754 width = Math.min(mMaxWidth, width); 755 } 756 break; 757 case MeasureSpec.UNSPECIFIED: 758 // Use maximum width, if specified, else preferred width 759 width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth(); 760 break; 761 } 762 widthMode = MeasureSpec.EXACTLY; 763 super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), heightMeasureSpec); 764 } 765 766 private int getPreferredWidth() { 767 return getContext().getResources() 768 .getDimensionPixelSize(R.dimen.abc_search_view_preferred_width); 769 } 770 771 private void updateViewsVisibility(final boolean collapsed) { 772 mIconified = collapsed; 773 // Visibility of views that are visible when collapsed 774 final int visCollapsed = collapsed ? VISIBLE : GONE; 775 // Is there text in the query 776 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); 777 778 mSearchButton.setVisibility(visCollapsed); 779 updateSubmitButton(hasText); 780 mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE); 781 mSearchHintIcon.setVisibility(mIconifiedByDefault ? GONE : VISIBLE); 782 updateCloseButton(); 783 updateVoiceButton(!hasText); 784 updateSubmitArea(); 785 } 786 787 private boolean hasVoiceSearch() { 788 if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) { 789 Intent testIntent = null; 790 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 791 testIntent = mVoiceWebSearchIntent; 792 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 793 testIntent = mVoiceAppSearchIntent; 794 } 795 if (testIntent != null) { 796 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, 797 PackageManager.MATCH_DEFAULT_ONLY); 798 return ri != null; 799 } 800 } 801 return false; 802 } 803 804 private boolean isSubmitAreaEnabled() { 805 return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified(); 806 } 807 808 private void updateSubmitButton(boolean hasText) { 809 int visibility = GONE; 810 if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus() 811 && (hasText || !mVoiceButtonEnabled)) { 812 visibility = VISIBLE; 813 } 814 mSubmitButton.setVisibility(visibility); 815 } 816 817 private void updateSubmitArea() { 818 int visibility = GONE; 819 if (isSubmitAreaEnabled() 820 && (mSubmitButton.getVisibility() == VISIBLE 821 || mVoiceButton.getVisibility() == VISIBLE)) { 822 visibility = VISIBLE; 823 } 824 mSubmitArea.setVisibility(visibility); 825 } 826 827 private void updateCloseButton() { 828 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); 829 // Should we show the close button? It is not shown if there's no focus, 830 // field is not iconified by default and there is no text in it. 831 final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView); 832 mCloseButton.setVisibility(showClose ? VISIBLE : GONE); 833 mCloseButton.getDrawable().setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET); 834 } 835 836 private void postUpdateFocusedState() { 837 post(mUpdateDrawableStateRunnable); 838 } 839 840 private void updateFocusedState() { 841 boolean focused = mQueryTextView.hasFocus(); 842 mSearchPlate.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET); 843 mSubmitArea.getBackground().setState(focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET); 844 invalidate(); 845 } 846 847 @Override 848 protected void onDetachedFromWindow() { 849 removeCallbacks(mUpdateDrawableStateRunnable); 850 post(mReleaseCursorRunnable); 851 super.onDetachedFromWindow(); 852 } 853 854 private void setImeVisibility(final boolean visible) { 855 if (visible) { 856 post(mShowImeRunnable); 857 } else { 858 removeCallbacks(mShowImeRunnable); 859 InputMethodManager imm = (InputMethodManager) 860 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 861 862 if (imm != null) { 863 imm.hideSoftInputFromWindow(getWindowToken(), 0); 864 } 865 } 866 } 867 868 /** 869 * Called by the SuggestionsAdapter 870 * @hide 871 */ 872 /* package */void onQueryRefine(CharSequence queryText) { 873 setQuery(queryText); 874 } 875 876 private final OnClickListener mOnClickListener = new OnClickListener() { 877 878 public void onClick(View v) { 879 if (v == mSearchButton) { 880 onSearchClicked(); 881 } else if (v == mCloseButton) { 882 onCloseClicked(); 883 } else if (v == mSubmitButton) { 884 onSubmitQuery(); 885 } else if (v == mVoiceButton) { 886 onVoiceClicked(); 887 } else if (v == mQueryTextView) { 888 forceSuggestionQuery(); 889 } 890 } 891 }; 892 893 /** 894 * Handles the key down event for dealing with action keys. 895 * 896 * @param keyCode This is the keycode of the typed key, and is the same value as 897 * found in the KeyEvent parameter. 898 * @param event The complete event record for the typed key 899 * 900 * @return true if the event was handled here, or false if not. 901 */ 902 @Override 903 public boolean onKeyDown(int keyCode, KeyEvent event) { 904 if (mSearchable == null) { 905 return false; 906 } 907 908 return super.onKeyDown(keyCode, event); 909 } 910 911 /** 912 * React to the user typing "enter" or other hardwired keys while typing in 913 * the search box. This handles these special keys while the edit box has 914 * focus. 915 */ 916 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { 917 public boolean onKey(View v, int keyCode, KeyEvent event) { 918 // guard against possible race conditions 919 if (mSearchable == null) { 920 return false; 921 } 922 923 if (DBG) { 924 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: " 925 + mQueryTextView.getListSelection()); 926 } 927 928 // If a suggestion is selected, handle enter, search key, and action keys 929 // as presses on the selected suggestion 930 if (mQueryTextView.isPopupShowing() 931 && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) { 932 return onSuggestionsKey(v, keyCode, event); 933 } 934 935 // If there is text in the query box, handle enter, and action keys 936 // The search key is handled by the dialog's onKeyDown(). 937 if (!mQueryTextView.isEmpty() && KeyEventCompat.hasNoModifiers(event)) { 938 if (event.getAction() == KeyEvent.ACTION_UP) { 939 if (keyCode == KeyEvent.KEYCODE_ENTER) { 940 v.cancelLongPress(); 941 942 // Launch as a regular search. 943 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText() 944 .toString()); 945 return true; 946 } 947 } 948 } 949 return false; 950 } 951 }; 952 953 /** 954 * React to the user typing while in the suggestions list. First, check for 955 * action keys. If not handled, try refocusing regular characters into the 956 * EditText. 957 */ 958 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { 959 // guard against possible race conditions (late arrival after dismiss) 960 if (mSearchable == null) { 961 return false; 962 } 963 if (mSuggestionsAdapter == null) { 964 return false; 965 } 966 if (event.getAction() == KeyEvent.ACTION_DOWN && KeyEventCompat.hasNoModifiers(event)) { 967 // First, check for enter or search (both of which we'll treat as a 968 // "click") 969 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH 970 || keyCode == KeyEvent.KEYCODE_TAB) { 971 int position = mQueryTextView.getListSelection(); 972 return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); 973 } 974 975 // Next, check for left/right moves, which we use to "return" the 976 // user to the edit view 977 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 978 // give "focus" to text editor, with cursor at the beginning if 979 // left key, at end if right key 980 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView 981 .length(); 982 mQueryTextView.setSelection(selPoint); 983 mQueryTextView.setListSelection(0); 984 mQueryTextView.clearListSelection(); 985 HIDDEN_METHOD_INVOKER.ensureImeVisible(mQueryTextView, true); 986 987 return true; 988 } 989 990 // Next, check for an "up and out" move 991 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) { 992 // TODO: restoreUserQuery(); 993 // let ACTV complete the move 994 return false; 995 } 996 } 997 return false; 998 } 999 1000 private int getSearchIconId() { 1001 TypedValue outValue = new TypedValue(); 1002 getContext().getTheme().resolveAttribute(R.attr.searchViewSearchIcon, outValue, true); 1003 return outValue.resourceId; 1004 } 1005 1006 private CharSequence getDecoratedHint(CharSequence hintText) { 1007 // If the field is always expanded, then don't add the search icon to the hint 1008 if (!mIconifiedByDefault) return hintText; 1009 1010 SpannableStringBuilder ssb = new SpannableStringBuilder(" "); // for the icon 1011 ssb.append(hintText); 1012 Drawable searchIcon = getContext().getResources().getDrawable(getSearchIconId()); 1013 int textSize = (int) (mQueryTextView.getTextSize() * 1.25); 1014 searchIcon.setBounds(0, 0, textSize, textSize); 1015 ssb.setSpan(new ImageSpan(searchIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 1016 return ssb; 1017 } 1018 1019 private void updateQueryHint() { 1020 if (mQueryHint != null) { 1021 mQueryTextView.setHint(getDecoratedHint(mQueryHint)); 1022 } else if (mSearchable != null) { 1023 CharSequence hint = null; 1024 int hintId = mSearchable.getHintId(); 1025 if (hintId != 0) { 1026 hint = getContext().getString(hintId); 1027 } 1028 if (hint != null) { 1029 mQueryTextView.setHint(getDecoratedHint(hint)); 1030 } 1031 } else { 1032 mQueryTextView.setHint(getDecoratedHint("")); 1033 } 1034 } 1035 1036 /** 1037 * Updates the auto-complete text view. 1038 */ 1039 private void updateSearchAutoComplete() { 1040 mQueryTextView.setThreshold(mSearchable.getSuggestThreshold()); 1041 mQueryTextView.setImeOptions(mSearchable.getImeOptions()); 1042 int inputType = mSearchable.getInputType(); 1043 // We only touch this if the input type is set up for text (which it almost certainly 1044 // should be, in the case of search!) 1045 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { 1046 // The existence of a suggestions authority is the proxy for "suggestions 1047 // are available here" 1048 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 1049 if (mSearchable.getSuggestAuthority() != null) { 1050 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 1051 // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing 1052 // auto-completion based on its own semantics, which it will present to the user 1053 // as they type. This generally means that the input method should not show its 1054 // own candidates, and the spell checker should not be in action. The text editor 1055 // supplies its candidates by calling InputMethodManager.displayCompletions(), 1056 // which in turn will call InputMethodSession.displayCompletions(). 1057 inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; 1058 } 1059 } 1060 mQueryTextView.setInputType(inputType); 1061 if (mSuggestionsAdapter != null) { 1062 mSuggestionsAdapter.changeCursor(null); 1063 } 1064 // attach the suggestions adapter, if suggestions are available 1065 // The existence of a suggestions authority is the proxy for "suggestions available here" 1066 if (mSearchable.getSuggestAuthority() != null) { 1067 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), 1068 this, mSearchable, mOutsideDrawablesCache); 1069 mQueryTextView.setAdapter(mSuggestionsAdapter); 1070 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( 1071 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL 1072 : SuggestionsAdapter.REFINE_BY_ENTRY); 1073 } 1074 } 1075 1076 /** 1077 * Update the visibility of the voice button. There are actually two voice search modes, 1078 * either of which will activate the button. 1079 * @param empty whether the search query text field is empty. If it is, then the other 1080 * criteria apply to make the voice button visible. 1081 */ 1082 private void updateVoiceButton(boolean empty) { 1083 int visibility = GONE; 1084 if (mVoiceButtonEnabled && !isIconified() && empty) { 1085 visibility = VISIBLE; 1086 mSubmitButton.setVisibility(GONE); 1087 } 1088 mVoiceButton.setVisibility(visibility); 1089 } 1090 1091 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { 1092 1093 /** 1094 * Called when the input method default action key is pressed. 1095 */ 1096 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 1097 onSubmitQuery(); 1098 return true; 1099 } 1100 }; 1101 1102 private void onTextChanged(CharSequence newText) { 1103 CharSequence text = mQueryTextView.getText(); 1104 mUserQuery = text; 1105 boolean hasText = !TextUtils.isEmpty(text); 1106 updateSubmitButton(hasText); 1107 updateVoiceButton(!hasText); 1108 updateCloseButton(); 1109 updateSubmitArea(); 1110 if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) { 1111 mOnQueryChangeListener.onQueryTextChange(newText.toString()); 1112 } 1113 mOldQueryText = newText.toString(); 1114 } 1115 1116 private void onSubmitQuery() { 1117 CharSequence query = mQueryTextView.getText(); 1118 if (query != null && TextUtils.getTrimmedLength(query) > 0) { 1119 if (mOnQueryChangeListener == null 1120 || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { 1121 if (mSearchable != null) { 1122 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); 1123 setImeVisibility(false); 1124 } 1125 dismissSuggestions(); 1126 } 1127 } 1128 } 1129 1130 private void dismissSuggestions() { 1131 mQueryTextView.dismissDropDown(); 1132 } 1133 1134 private void onCloseClicked() { 1135 CharSequence text = mQueryTextView.getText(); 1136 if (TextUtils.isEmpty(text)) { 1137 if (mIconifiedByDefault) { 1138 // If the app doesn't override the close behavior 1139 if (mOnCloseListener == null || !mOnCloseListener.onClose()) { 1140 // hide the keyboard and remove focus 1141 clearFocus(); 1142 // collapse the search field 1143 updateViewsVisibility(true); 1144 } 1145 } 1146 } else { 1147 mQueryTextView.setText(""); 1148 mQueryTextView.requestFocus(); 1149 setImeVisibility(true); 1150 } 1151 1152 } 1153 1154 private void onSearchClicked() { 1155 updateViewsVisibility(false); 1156 mQueryTextView.requestFocus(); 1157 setImeVisibility(true); 1158 if (mOnSearchClickListener != null) { 1159 mOnSearchClickListener.onClick(this); 1160 } 1161 } 1162 1163 private void onVoiceClicked() { 1164 // guard against possible race conditions 1165 if (mSearchable == null) { 1166 return; 1167 } 1168 SearchableInfo searchable = mSearchable; 1169 try { 1170 if (searchable.getVoiceSearchLaunchWebSearch()) { 1171 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, 1172 searchable); 1173 getContext().startActivity(webSearchIntent); 1174 } else if (searchable.getVoiceSearchLaunchRecognizer()) { 1175 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, 1176 searchable); 1177 getContext().startActivity(appSearchIntent); 1178 } 1179 } catch (ActivityNotFoundException e) { 1180 // Should not happen, since we check the availability of 1181 // voice search before showing the button. But just in case... 1182 Log.w(LOG_TAG, "Could not find voice search activity"); 1183 } 1184 } 1185 1186 void onTextFocusChanged() { 1187 updateViewsVisibility(isIconified()); 1188 // Delayed update to make sure that the focus has settled down and window focus changes 1189 // don't affect it. A synchronous update was not working. 1190 postUpdateFocusedState(); 1191 if (mQueryTextView.hasFocus()) { 1192 forceSuggestionQuery(); 1193 } 1194 } 1195 1196 @Override 1197 public void onWindowFocusChanged(boolean hasWindowFocus) { 1198 super.onWindowFocusChanged(hasWindowFocus); 1199 1200 postUpdateFocusedState(); 1201 } 1202 1203 /** 1204 * {@inheritDoc} 1205 */ 1206 @Override 1207 public void onActionViewCollapsed() { 1208 clearFocus(); 1209 updateViewsVisibility(true); 1210 mQueryTextView.setImeOptions(mCollapsedImeOptions); 1211 mExpandedInActionView = false; 1212 } 1213 1214 /** 1215 * {@inheritDoc} 1216 */ 1217 @Override 1218 public void onActionViewExpanded() { 1219 if (mExpandedInActionView) return; 1220 1221 mExpandedInActionView = true; 1222 mCollapsedImeOptions = mQueryTextView.getImeOptions(); 1223 mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN); 1224 mQueryTextView.setText(""); 1225 setIconified(false); 1226 } 1227 1228 1229 private void adjustDropDownSizeAndPosition() { 1230 if (mDropDownAnchor.getWidth() > 1) { 1231 Resources res = getContext().getResources(); 1232 int anchorPadding = mSearchPlate.getPaddingLeft(); 1233 Rect dropDownPadding = new Rect(); 1234 int iconOffset = mIconifiedByDefault 1235 ? res.getDimensionPixelSize(R.dimen.abc_dropdownitem_icon_width) 1236 + res.getDimensionPixelSize(R.dimen.abc_dropdownitem_text_padding_left) 1237 : 0; 1238 mQueryTextView.getDropDownBackground().getPadding(dropDownPadding); 1239 int offset = anchorPadding - (dropDownPadding.left + iconOffset); 1240 mQueryTextView.setDropDownHorizontalOffset(offset); 1241 final int width = mDropDownAnchor.getWidth() + dropDownPadding.left 1242 + dropDownPadding.right + iconOffset - anchorPadding; 1243 mQueryTextView.setDropDownWidth(width); 1244 } 1245 } 1246 1247 private boolean onItemClicked(int position, int actionKey, String actionMsg) { 1248 if (mOnSuggestionListener == null 1249 || !mOnSuggestionListener.onSuggestionClick(position)) { 1250 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); 1251 setImeVisibility(false); 1252 dismissSuggestions(); 1253 return true; 1254 } 1255 return false; 1256 } 1257 1258 private boolean onItemSelected(int position) { 1259 if (mOnSuggestionListener == null 1260 || !mOnSuggestionListener.onSuggestionSelect(position)) { 1261 rewriteQueryFromSuggestion(position); 1262 return true; 1263 } 1264 return false; 1265 } 1266 1267 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { 1268 1269 /** 1270 * Implements OnItemClickListener 1271 */ 1272 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1273 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); 1274 onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); 1275 } 1276 }; 1277 1278 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { 1279 1280 /** 1281 * Implements OnItemSelectedListener 1282 */ 1283 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1284 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); 1285 SearchView.this.onItemSelected(position); 1286 } 1287 1288 /** 1289 * Implements OnItemSelectedListener 1290 */ 1291 public void onNothingSelected(AdapterView<?> parent) { 1292 if (DBG) 1293 Log.d(LOG_TAG, "onNothingSelected()"); 1294 } 1295 }; 1296 1297 /** 1298 * Query rewriting. 1299 */ 1300 private void rewriteQueryFromSuggestion(int position) { 1301 CharSequence oldQuery = mQueryTextView.getText(); 1302 Cursor c = mSuggestionsAdapter.getCursor(); 1303 if (c == null) { 1304 return; 1305 } 1306 if (c.moveToPosition(position)) { 1307 // Get the new query from the suggestion. 1308 CharSequence newQuery = mSuggestionsAdapter.convertToString(c); 1309 if (newQuery != null) { 1310 // The suggestion rewrites the query. 1311 // Update the text field, without getting new suggestions. 1312 setQuery(newQuery); 1313 } else { 1314 // The suggestion does not rewrite the query, restore the user's query. 1315 setQuery(oldQuery); 1316 } 1317 } else { 1318 // We got a bad position, restore the user's query. 1319 setQuery(oldQuery); 1320 } 1321 } 1322 1323 /** 1324 * Launches an intent based on a suggestion. 1325 * 1326 * @param position The index of the suggestion to create the intent from. 1327 * @param actionKey The key code of the action key that was pressed, 1328 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1329 * @param actionMsg The message for the action key that was pressed, 1330 * or <code>null</code> if none. 1331 * @return true if a successful launch, false if could not (e.g. bad position). 1332 */ 1333 private boolean launchSuggestion(int position, int actionKey, String actionMsg) { 1334 Cursor c = mSuggestionsAdapter.getCursor(); 1335 if ((c != null) && c.moveToPosition(position)) { 1336 1337 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); 1338 1339 // launch the intent 1340 launchIntent(intent); 1341 1342 return true; 1343 } 1344 return false; 1345 } 1346 1347 /** 1348 * Launches an intent, including any special intent handling. 1349 */ 1350 private void launchIntent(Intent intent) { 1351 if (intent == null) { 1352 return; 1353 } 1354 try { 1355 // If the intent was created from a suggestion, it will always have an explicit 1356 // component here. 1357 getContext().startActivity(intent); 1358 } catch (RuntimeException ex) { 1359 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 1360 } 1361 } 1362 1363 /** 1364 * Sets the text in the query box, without updating the suggestions. 1365 */ 1366 private void setQuery(CharSequence query) { 1367 mQueryTextView.setText(query); 1368 // Move the cursor to the end 1369 mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length()); 1370 } 1371 1372 private void launchQuerySearch(int actionKey, String actionMsg, String query) { 1373 String action = Intent.ACTION_SEARCH; 1374 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); 1375 getContext().startActivity(intent); 1376 } 1377 1378 /** 1379 * Constructs an intent from the given information and the search dialog state. 1380 * 1381 * @param action Intent action. 1382 * @param data Intent data, or <code>null</code>. 1383 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 1384 * @param query Intent query, or <code>null</code>. 1385 * @param actionKey The key code of the action key that was pressed, 1386 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1387 * @param actionMsg The message for the action key that was pressed, 1388 * or <code>null</code> if none. 1389 * @return The intent. 1390 */ 1391 private Intent createIntent(String action, Uri data, String extraData, String query, 1392 int actionKey, String actionMsg) { 1393 // Now build the Intent 1394 Intent intent = new Intent(action); 1395 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1396 // We need CLEAR_TOP to avoid reusing an old task that has other activities 1397 // on top of the one we want. We don't want to do this in in-app search though, 1398 // as it can be destructive to the activity stack. 1399 if (data != null) { 1400 intent.setData(data); 1401 } 1402 intent.putExtra(SearchManager.USER_QUERY, mUserQuery); 1403 if (query != null) { 1404 intent.putExtra(SearchManager.QUERY, query); 1405 } 1406 if (extraData != null) { 1407 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 1408 } 1409 if (mAppSearchData != null) { 1410 intent.putExtra(SearchManager.APP_DATA, mAppSearchData); 1411 } 1412 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 1413 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 1414 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 1415 } 1416 intent.setComponent(mSearchable.getSearchActivity()); 1417 return intent; 1418 } 1419 1420 /** 1421 * Create and return an Intent that can launch the voice search activity for web search. 1422 */ 1423 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { 1424 Intent voiceIntent = new Intent(baseIntent); 1425 ComponentName searchActivity = searchable.getSearchActivity(); 1426 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null 1427 : searchActivity.flattenToShortString()); 1428 return voiceIntent; 1429 } 1430 1431 /** 1432 * Create and return an Intent that can launch the voice search activity, perform a specific 1433 * voice transcription, and forward the results to the searchable activity. 1434 * 1435 * @param baseIntent The voice app search intent to start from 1436 * @return A completely-configured intent ready to send to the voice search activity 1437 */ 1438 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { 1439 ComponentName searchActivity = searchable.getSearchActivity(); 1440 1441 // create the necessary intent to set up a search-and-forward operation 1442 // in the voice search system. We have to keep the bundle separate, 1443 // because it becomes immutable once it enters the PendingIntent 1444 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 1445 queryIntent.setComponent(searchActivity); 1446 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, 1447 PendingIntent.FLAG_ONE_SHOT); 1448 1449 // Now set up the bundle that will be inserted into the pending intent 1450 // when it's time to do the search. We always build it here (even if empty) 1451 // because the voice search activity will always need to insert "QUERY" into 1452 // it anyway. 1453 Bundle queryExtras = new Bundle(); 1454 if (mAppSearchData != null) { 1455 queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData); 1456 } 1457 1458 // Now build the intent to launch the voice search. Add all necessary 1459 // extras to launch the voice recognizer, and then all the necessary extras 1460 // to forward the results to the searchable activity 1461 Intent voiceIntent = new Intent(baseIntent); 1462 1463 // Add all of the configuration options supplied by the searchable's metadata 1464 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 1465 String prompt = null; 1466 String language = null; 1467 int maxResults = 1; 1468 1469 Resources resources = getResources(); 1470 if (searchable.getVoiceLanguageModeId() != 0) { 1471 languageModel = resources.getString(searchable.getVoiceLanguageModeId()); 1472 } 1473 if (searchable.getVoicePromptTextId() != 0) { 1474 prompt = resources.getString(searchable.getVoicePromptTextId()); 1475 } 1476 if (searchable.getVoiceLanguageId() != 0) { 1477 language = resources.getString(searchable.getVoiceLanguageId()); 1478 } 1479 if (searchable.getVoiceMaxResults() != 0) { 1480 maxResults = searchable.getVoiceMaxResults(); 1481 } 1482 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 1483 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 1484 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 1485 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 1486 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null 1487 : searchActivity.flattenToShortString()); 1488 1489 // Add the values that configure forwarding the results 1490 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 1491 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 1492 1493 return voiceIntent; 1494 } 1495 1496 /** 1497 * When a particular suggestion has been selected, perform the various lookups required 1498 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 1499 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 1500 * the suggestion includes a data id. 1501 * 1502 * @param c The suggestions cursor, moved to the row of the user's selection 1503 * @param actionKey The key code of the action key that was pressed, 1504 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1505 * @param actionMsg The message for the action key that was pressed, 1506 * or <code>null</code> if none. 1507 * @return An intent for the suggestion at the cursor's position. 1508 */ 1509 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { 1510 try { 1511 // use specific action if supplied, or default action if supplied, or fixed default 1512 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 1513 1514 if (action == null) { 1515 action = mSearchable.getSuggestIntentAction(); 1516 } 1517 if (action == null) { 1518 action = Intent.ACTION_SEARCH; 1519 } 1520 1521 // use specific data if supplied, or default data if supplied 1522 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1523 if (data == null) { 1524 data = mSearchable.getSuggestIntentData(); 1525 } 1526 // then, if an ID was provided, append it. 1527 if (data != null) { 1528 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 1529 if (id != null) { 1530 data = data + "/" + Uri.encode(id); 1531 } 1532 } 1533 Uri dataUri = (data == null) ? null : Uri.parse(data); 1534 1535 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); 1536 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 1537 1538 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); 1539 } catch (RuntimeException e ) { 1540 int rowNum; 1541 try { // be really paranoid now 1542 rowNum = c.getPosition(); 1543 } catch (RuntimeException e2 ) { 1544 rowNum = -1; 1545 } 1546 Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum + 1547 " returned exception.", e); 1548 return null; 1549 } 1550 } 1551 1552 private void forceSuggestionQuery() { 1553 HIDDEN_METHOD_INVOKER.doBeforeTextChanged(mQueryTextView); 1554 HIDDEN_METHOD_INVOKER.doAfterTextChanged(mQueryTextView); 1555 } 1556 1557 static boolean isLandscapeMode(Context context) { 1558 return context.getResources().getConfiguration().orientation 1559 == Configuration.ORIENTATION_LANDSCAPE; 1560 } 1561 1562 /** 1563 * Callback to watch the text field for empty/non-empty 1564 */ 1565 private TextWatcher mTextWatcher = new TextWatcher() { 1566 1567 public void beforeTextChanged(CharSequence s, int start, int before, int after) { } 1568 1569 public void onTextChanged(CharSequence s, int start, 1570 int before, int after) { 1571 SearchView.this.onTextChanged(s); 1572 } 1573 1574 public void afterTextChanged(Editable s) { 1575 } 1576 }; 1577 1578 /** 1579 * Local subclass for AutoCompleteTextView. 1580 * @hide 1581 */ 1582 public static class SearchAutoComplete extends AutoCompleteTextView { 1583 1584 private int mThreshold; 1585 private SearchView mSearchView; 1586 1587 public SearchAutoComplete(Context context) { 1588 super(context); 1589 mThreshold = getThreshold(); 1590 } 1591 1592 public SearchAutoComplete(Context context, AttributeSet attrs) { 1593 super(context, attrs); 1594 mThreshold = getThreshold(); 1595 } 1596 1597 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1598 super(context, attrs, defStyle); 1599 mThreshold = getThreshold(); 1600 } 1601 1602 void setSearchView(SearchView searchView) { 1603 mSearchView = searchView; 1604 } 1605 1606 @Override 1607 public void setThreshold(int threshold) { 1608 super.setThreshold(threshold); 1609 mThreshold = threshold; 1610 } 1611 1612 /** 1613 * Returns true if the text field is empty, or contains only whitespace. 1614 */ 1615 private boolean isEmpty() { 1616 return TextUtils.getTrimmedLength(getText()) == 0; 1617 } 1618 1619 /** 1620 * We override this method to avoid replacing the query box text when a 1621 * suggestion is clicked. 1622 */ 1623 @Override 1624 protected void replaceText(CharSequence text) { 1625 } 1626 1627 /** 1628 * We override this method to avoid an extra onItemClick being called on 1629 * the drop-down's OnItemClickListener by 1630 * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is 1631 * clicked with the trackball. 1632 */ 1633 @Override 1634 public void performCompletion() { 1635 } 1636 1637 /** 1638 * We override this method to be sure and show the soft keyboard if 1639 * appropriate when the TextView has focus. 1640 */ 1641 @Override 1642 public void onWindowFocusChanged(boolean hasWindowFocus) { 1643 super.onWindowFocusChanged(hasWindowFocus); 1644 1645 if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) { 1646 InputMethodManager inputManager = (InputMethodManager) getContext() 1647 .getSystemService(Context.INPUT_METHOD_SERVICE); 1648 inputManager.showSoftInput(this, 0); 1649 // If in landscape mode, then make sure that 1650 // the ime is in front of the dropdown. 1651 if (isLandscapeMode(getContext())) { 1652 HIDDEN_METHOD_INVOKER.ensureImeVisible(this, true); 1653 } 1654 } 1655 } 1656 1657 @Override 1658 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 1659 super.onFocusChanged(focused, direction, previouslyFocusedRect); 1660 mSearchView.onTextFocusChanged(); 1661 } 1662 1663 /** 1664 * We override this method so that we can allow a threshold of zero, 1665 * which ACTV does not. 1666 */ 1667 @Override 1668 public boolean enoughToFilter() { 1669 return mThreshold <= 0 || super.enoughToFilter(); 1670 } 1671 1672 @Override 1673 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1674 if (keyCode == KeyEvent.KEYCODE_BACK) { 1675 // special case for the back key, we do not even try to send it 1676 // to the drop down list but instead, consume it immediately 1677 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 1678 KeyEvent.DispatcherState state = getKeyDispatcherState(); 1679 if (state != null) { 1680 state.startTracking(event, this); 1681 } 1682 return true; 1683 } else if (event.getAction() == KeyEvent.ACTION_UP) { 1684 KeyEvent.DispatcherState state = getKeyDispatcherState(); 1685 if (state != null) { 1686 state.handleUpEvent(event); 1687 } 1688 if (event.isTracking() && !event.isCanceled()) { 1689 mSearchView.clearFocus(); 1690 mSearchView.setImeVisibility(false); 1691 return true; 1692 } 1693 } 1694 } 1695 return super.onKeyPreIme(keyCode, event); 1696 } 1697 } 1698 1699 private static class AutoCompleteTextViewReflector { 1700 private Method doBeforeTextChanged, doAfterTextChanged; 1701 private Method ensureImeVisible; 1702 private Method showSoftInputUnchecked; 1703 1704 AutoCompleteTextViewReflector() { 1705 try { 1706 doBeforeTextChanged = AutoCompleteTextView.class 1707 .getDeclaredMethod("doBeforeTextChanged"); 1708 doBeforeTextChanged.setAccessible(true); 1709 } catch (NoSuchMethodException e) { 1710 // Ah well. 1711 } 1712 try { 1713 doAfterTextChanged = AutoCompleteTextView.class 1714 .getDeclaredMethod("doAfterTextChanged"); 1715 doAfterTextChanged.setAccessible(true); 1716 } catch (NoSuchMethodException e) { 1717 // Ah well. 1718 } 1719 try { 1720 ensureImeVisible = AutoCompleteTextView.class 1721 .getMethod("ensureImeVisible", boolean.class); 1722 ensureImeVisible.setAccessible(true); 1723 } catch (NoSuchMethodException e) { 1724 // Ah well. 1725 } 1726 try { 1727 showSoftInputUnchecked = InputMethodManager.class.getMethod( 1728 "showSoftInputUnchecked", int.class, ResultReceiver.class); 1729 showSoftInputUnchecked.setAccessible(true); 1730 } catch (NoSuchMethodException e) { 1731 // Ah well. 1732 } 1733 } 1734 1735 void doBeforeTextChanged(AutoCompleteTextView view) { 1736 if (doBeforeTextChanged != null) { 1737 try { 1738 doBeforeTextChanged.invoke(view); 1739 } catch (Exception e) { 1740 } 1741 } 1742 } 1743 1744 void doAfterTextChanged(AutoCompleteTextView view) { 1745 if (doAfterTextChanged != null) { 1746 try { 1747 doAfterTextChanged.invoke(view); 1748 } catch (Exception e) { 1749 } 1750 } 1751 } 1752 1753 void ensureImeVisible(AutoCompleteTextView view, boolean visible) { 1754 if (ensureImeVisible != null) { 1755 try { 1756 ensureImeVisible.invoke(view, visible); 1757 } catch (Exception e) { 1758 } 1759 } 1760 } 1761 1762 void showSoftInputUnchecked(InputMethodManager imm, View view, int flags) { 1763 if (showSoftInputUnchecked != null) { 1764 try { 1765 showSoftInputUnchecked.invoke(imm, flags, null); 1766 return; 1767 } catch (Exception e) { 1768 } 1769 } 1770 1771 // Hidden method failed, call public version instead 1772 imm.showSoftInput(view, flags); 1773 } 1774 } 1775}