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