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