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