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