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