SearchView.java revision ebcf5a3a50b84320528af5c2a57db99f76c08af5
1/* 2 * Copyright (C) 2010 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.widget; 18 19import static android.widget.SuggestionsAdapter.getColumnString; 20 21import com.android.internal.R; 22 23import android.app.PendingIntent; 24import android.app.SearchManager; 25import android.app.SearchableInfo; 26import android.content.ActivityNotFoundException; 27import android.content.ComponentName; 28import android.content.Context; 29import android.content.Intent; 30import android.content.pm.PackageManager; 31import android.content.pm.ResolveInfo; 32import android.content.res.Resources; 33import android.content.res.TypedArray; 34import android.database.Cursor; 35import android.graphics.Rect; 36import android.graphics.drawable.Drawable; 37import android.net.Uri; 38import android.os.Bundle; 39import android.speech.RecognizerIntent; 40import android.text.Editable; 41import android.text.TextUtils; 42import android.text.TextWatcher; 43import android.util.AttributeSet; 44import android.util.Log; 45import android.view.KeyEvent; 46import android.view.LayoutInflater; 47import android.view.View; 48import android.view.inputmethod.InputMethodManager; 49import android.widget.AdapterView.OnItemClickListener; 50import android.widget.AdapterView.OnItemSelectedListener; 51import android.widget.TextView.OnEditorActionListener; 52 53import java.util.WeakHashMap; 54 55/** 56 * Provides the user interface elements for the user to enter a search query and submit a 57 * request to a search provider. Shows a list of query suggestions or results, if 58 * available and allows the user to pick a suggestion or result to launch into. 59 */ 60public class SearchView extends LinearLayout { 61 62 private static final boolean DBG = false; 63 private static final String LOG_TAG = "SearchView"; 64 65 private OnQueryChangeListener mOnQueryChangeListener; 66 private OnCloseListener mOnCloseListener; 67 private OnFocusChangeListener mOnQueryTextFocusChangeListener; 68 private OnSuggestionSelectionListener mOnSuggestionListener; 69 70 private boolean mIconifiedByDefault; 71 private boolean mIconified; 72 private CursorAdapter mSuggestionsAdapter; 73 private View mSearchButton; 74 private View mSubmitButton; 75 private View mCloseButton; 76 private View mSearchEditFrame; 77 private View mVoiceButton; 78 private AutoCompleteTextView mQueryTextView; 79 private boolean mSubmitButtonEnabled; 80 private CharSequence mQueryHint; 81 private boolean mQueryRefinement; 82 private boolean mClearingFocus; 83 84 private SearchableInfo mSearchable; 85 86 // For voice searching 87 private final Intent mVoiceWebSearchIntent; 88 private final Intent mVoiceAppSearchIntent; 89 90 // A weak map of drawables we've gotten from other packages, so we don't load them 91 // more than once. 92 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = 93 new WeakHashMap<String, Drawable.ConstantState>(); 94 95 /** 96 * Callbacks for changes to the query text. 97 */ 98 public interface OnQueryChangeListener { 99 100 /** 101 * Called when the user submits the query. This could be due to a key press on the 102 * keyboard or due to pressing a submit button. 103 * The listener can override the standard behavior by returning true 104 * to indicate that it has handled the submit request. Otherwise return false to 105 * let the SearchView handle the submission by launching any associated intent. 106 * 107 * @param query the query text that is to be submitted 108 * 109 * @return true if the query has been handled by the listener, false to let the 110 * SearchView perform the default action. 111 */ 112 boolean onSubmitQuery(String query); 113 114 /** 115 * Called when the query text is changed by the user. 116 * 117 * @param newText the new content of the query text field. 118 * 119 * @return false if the SearchView should perform the default action of showing any 120 * suggestions if available, true if the action was handled by the listener. 121 */ 122 boolean onQueryTextChanged(String newText); 123 } 124 125 public interface OnCloseListener { 126 127 /** 128 * The user is attempting to close the SearchView. 129 * 130 * @return true if the listener wants to override the default behavior of clearing the 131 * text field and dismissing it, false otherwise. 132 */ 133 boolean onClose(); 134 } 135 136 /** 137 * Callback interface for selection events on suggestions. These callbacks 138 * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}. 139 */ 140 public interface OnSuggestionSelectionListener { 141 142 /** 143 * Called when a suggestion was selected by navigating to it. 144 * @param position the absolute position in the list of suggestions. 145 * 146 * @return true if the listener handles the event and wants to override the default 147 * behavior of possibly rewriting the query based on the selected item, false otherwise. 148 */ 149 boolean onSuggestionSelected(int position); 150 151 /** 152 * Called when a suggestion was clicked. 153 * @param position the absolute position of the clicked item in the list of suggestions. 154 * 155 * @return true if the listener handles the event and wants to override the default 156 * behavior of launching any intent or submitting a search query specified on that item. 157 * Return false otherwise. 158 */ 159 boolean onSuggestionClicked(int position); 160 } 161 162 public SearchView(Context context) { 163 this(context, null); 164 } 165 166 public SearchView(Context context, AttributeSet attrs) { 167 super(context, attrs); 168 169 LayoutInflater inflater = (LayoutInflater) context 170 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 171 inflater.inflate(R.layout.search_view, this, true); 172 173 mSearchButton = findViewById(R.id.search_button); 174 mQueryTextView = (AutoCompleteTextView) findViewById(R.id.search_src_text); 175 mSearchEditFrame = findViewById(R.id.search_edit_frame); 176 mSubmitButton = findViewById(R.id.search_go_btn); 177 mCloseButton = findViewById(R.id.search_close_btn); 178 mVoiceButton = findViewById(R.id.search_voice_btn); 179 180 mSearchButton.setOnClickListener(mOnClickListener); 181 mCloseButton.setOnClickListener(mOnClickListener); 182 mSubmitButton.setOnClickListener(mOnClickListener); 183 mVoiceButton.setOnClickListener(mOnClickListener); 184 185 mQueryTextView.addTextChangedListener(mTextWatcher); 186 mQueryTextView.setOnEditorActionListener(mOnEditorActionListener); 187 mQueryTextView.setOnItemClickListener(mOnItemClickListener); 188 mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener); 189 // Inform any listener of focus changes 190 mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() { 191 192 public void onFocusChange(View v, boolean hasFocus) { 193 if (mOnQueryTextFocusChangeListener != null) { 194 mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus); 195 } 196 } 197 }); 198 199 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0); 200 setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true)); 201 a.recycle(); 202 203 // Save voice intent for later queries/launching 204 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); 205 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 206 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 207 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 208 209 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 210 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 211 212 updateViewsVisibility(mIconifiedByDefault); 213 } 214 215 /** 216 * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used 217 * to display labels, hints, suggestions, create intents for launching search results screens 218 * and controlling other affordances such as a voice button. 219 * 220 * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific 221 * activity or a global search provider. 222 */ 223 public void setSearchableInfo(SearchableInfo searchable) { 224 mSearchable = searchable; 225 if (mSearchable != null) { 226 updateSearchAutoComplete(); 227 } 228 updateViewsVisibility(mIconifiedByDefault); 229 } 230 231 /** @hide */ 232 @Override 233 public boolean requestFocus(int direction, Rect previouslyFocusedRect) { 234 if (mClearingFocus || isIconified()) return false; 235 return mQueryTextView.requestFocus(direction, previouslyFocusedRect); 236 } 237 238 /** @hide */ 239 @Override 240 public void clearFocus() { 241 mClearingFocus = true; 242 super.clearFocus(); 243 mQueryTextView.clearFocus(); 244 setImeVisibility(false); 245 mClearingFocus = false; 246 } 247 248 /** 249 * Sets a listener for user actions within the SearchView. 250 * 251 * @param listener the listener object that receives callbacks when the user performs 252 * actions in the SearchView such as clicking on buttons or typing a query. 253 */ 254 public void setOnQueryChangeListener(OnQueryChangeListener listener) { 255 mOnQueryChangeListener = listener; 256 } 257 258 /** 259 * Sets a listener to inform when the user closes the SearchView. 260 * 261 * @param listener the listener to call when the user closes the SearchView. 262 */ 263 public void setOnCloseListener(OnCloseListener listener) { 264 mOnCloseListener = listener; 265 } 266 267 /** 268 * Sets a listener to inform when the focus of the query text field changes. 269 * 270 * @param listener the listener to inform of focus changes. 271 */ 272 public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) { 273 mOnQueryTextFocusChangeListener = listener; 274 } 275 276 /** 277 * Sets a listener to inform when a suggestion is focused or clicked. 278 * 279 * @param listener the listener to inform of suggestion selection events. 280 */ 281 public void setOnSuggestionSelectionListener(OnSuggestionSelectionListener listener) { 282 mOnSuggestionListener = listener; 283 } 284 285 /** 286 * Sets a query string in the text field and optionally submits the query as well. 287 * 288 * @param query the query string. This replaces any query text already present in the 289 * text field. 290 * @param submit whether to submit the query right now or only update the contents of 291 * text field. 292 */ 293 public void setQuery(CharSequence query, boolean submit) { 294 mQueryTextView.setText(query); 295 // If the query is not empty and submit is requested, submit the query 296 if (submit && !TextUtils.isEmpty(query)) { 297 onSubmitQuery(); 298 } 299 } 300 301 /** 302 * Sets the hint text to display in the query text field. This overrides any hint specified 303 * in the SearchableInfo. 304 * 305 * @param hint the hint text to display 306 */ 307 public void setQueryHint(CharSequence hint) { 308 mQueryHint = hint; 309 updateQueryHint(); 310 } 311 312 /** 313 * Sets the default or resting state of the search field. If true, a single search icon is 314 * shown by default and expands to show the text field and other buttons when pressed. Also, 315 * if the default state is iconified, then it collapses to that state when the close button 316 * is pressed. Changes to this property will take effect immediately. 317 * 318 * <p>The default value is false.</p> 319 * 320 * @param iconified whether the search field should be iconified by default 321 */ 322 public void setIconifiedByDefault(boolean iconified) { 323 if (mIconifiedByDefault == iconified) return; 324 mIconifiedByDefault = iconified; 325 updateViewsVisibility(iconified); 326 setImeVisibility(!iconified); 327 } 328 329 /** 330 * Returns the default iconified state of the search field. 331 * @return 332 */ 333 public boolean isIconfiedByDefault() { 334 return mIconifiedByDefault; 335 } 336 337 /** 338 * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is 339 * a temporary state and does not override the default iconified state set by 340 * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then 341 * a false here will only be valid until the user closes the field. And if the default 342 * state is expanded, then a true here will only clear the text field and not close it. 343 * 344 * @param iconify a true value will collapse the SearchView to an icon, while a false will 345 * expand it. 346 */ 347 public void setIconified(boolean iconify) { 348 if (iconify) { 349 onCloseClicked(); 350 } else { 351 onSearchClicked(); 352 } 353 } 354 355 /** 356 * Returns the current iconified state of the SearchView. 357 * 358 * @return true if the SearchView is currently iconified, false if the search field is 359 * fully visible. 360 */ 361 public boolean isIconified() { 362 return mIconified; 363 } 364 365 /** 366 * Enables showing a submit button when the query is non-empty. In cases where the SearchView 367 * is being used to filter the contents of the current activity and doesn't launch a separate 368 * results activity, then the submit button should be disabled. 369 * 370 * @param enabled true to show a submit button for submitting queries, false if a submit 371 * button is not required. 372 */ 373 public void setSubmitButtonEnabled(boolean enabled) { 374 mSubmitButtonEnabled = enabled; 375 updateViewsVisibility(isIconified()); 376 } 377 378 /** 379 * Returns whether the submit button is enabled when necessary or never displayed. 380 * 381 * @return whether the submit button is enabled automatically when necessary 382 */ 383 public boolean isSubmitButtonEnabled() { 384 return mSubmitButtonEnabled; 385 } 386 387 /** 388 * Specifies if a query refinement button should be displayed alongside each suggestion 389 * or if it should depend on the flags set in the individual items retrieved from the 390 * suggestions provider. Clicking on the query refinement button will replace the text 391 * in the query text field with the text from the suggestion. This flag only takes effect 392 * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)} 393 * and not when using a custom adapter. 394 * 395 * @param enable true if all items should have a query refinement button, false if only 396 * those items that have a query refinement flag set should have the button. 397 * 398 * @see SearchManager#SUGGEST_COLUMN_FLAGS 399 * @see SearchManager#FLAG_QUERY_REFINEMENT 400 */ 401 public void setQueryRefinementEnabled(boolean enable) { 402 mQueryRefinement = enable; 403 if (mSuggestionsAdapter instanceof SuggestionsAdapter) { 404 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( 405 enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY); 406 } 407 } 408 409 /** 410 * Returns whether query refinement is enabled for all items or only specific ones. 411 * @return true if enabled for all items, false otherwise. 412 */ 413 public boolean isQueryRefinementEnabled() { 414 return mQueryRefinement; 415 } 416 417 /** 418 * You can set a custom adapter if you wish. Otherwise the default adapter is used to 419 * display the suggestions from the suggestions provider associated with the SearchableInfo. 420 * 421 * @see #setSearchableInfo(SearchableInfo) 422 */ 423 public void setSuggestionsAdapter(CursorAdapter adapter) { 424 mSuggestionsAdapter = adapter; 425 426 mQueryTextView.setAdapter(mSuggestionsAdapter); 427 } 428 429 /** 430 * Returns the adapter used for suggestions, if any. 431 * @return the suggestions adapter 432 */ 433 public CursorAdapter getSuggestionsAdapter() { 434 return mSuggestionsAdapter; 435 } 436 437 private void updateViewsVisibility(final boolean collapsed) { 438 mIconified = collapsed; 439 // Visibility of views that are visible when collapsed 440 final int visCollapsed = collapsed ? VISIBLE : GONE; 441 // Visibility of views that are visible when expanded 442 final int visExpanded = collapsed ? GONE : VISIBLE; 443 // Is there text in the query 444 final boolean hasText = !TextUtils.isEmpty(mQueryTextView.getText()); 445 446 mSearchButton.setVisibility(visCollapsed); 447 mSubmitButton.setVisibility(mSubmitButtonEnabled && hasText ? visExpanded : GONE); 448 mSearchEditFrame.setVisibility(visExpanded); 449 updateVoiceButton(!hasText); 450 } 451 452 private void setImeVisibility(boolean visible) { 453 InputMethodManager imm = (InputMethodManager) 454 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 455 456 // We made sure the IME was displayed, so also make sure it is closed 457 // when we go away. 458 if (imm != null) { 459 if (visible) { 460 imm.showSoftInputUnchecked(0, null); 461 } else { 462 imm.hideSoftInputFromWindow(getWindowToken(), 0); 463 } 464 } 465 } 466 467 /** 468 * Called by the SuggestionsAdapter 469 * @hide 470 */ 471 /* package */void onQueryRefine(CharSequence queryText) { 472 setQuery(queryText); 473 } 474 475 private final OnClickListener mOnClickListener = new OnClickListener() { 476 477 public void onClick(View v) { 478 if (v == mSearchButton) { 479 onSearchClicked(); 480 } else if (v == mCloseButton) { 481 onCloseClicked(); 482 } else if (v == mSubmitButton) { 483 onSubmitQuery(); 484 } else if (v == mVoiceButton) { 485 onVoiceClicked(); 486 } 487 } 488 }; 489 490 /** 491 * Handles the key down event for dealing with action keys. 492 * 493 * @param keyCode This is the keycode of the typed key, and is the same value as 494 * found in the KeyEvent parameter. 495 * @param event The complete event record for the typed key 496 * 497 * @return true if the event was handled here, or false if not. 498 */ 499 @Override 500 public boolean onKeyDown(int keyCode, KeyEvent event) { 501 if (mSearchable == null) { 502 return false; 503 } 504 505 // if it's an action specified by the searchable activity, launch the 506 // entered query with the action key 507 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 508 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 509 launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText() 510 .toString()); 511 return true; 512 } 513 514 return super.onKeyDown(keyCode, event); 515 } 516 517 private void updateQueryHint() { 518 if (mQueryHint != null) { 519 mQueryTextView.setHint(mQueryHint); 520 } else if (mSearchable != null) { 521 CharSequence hint = null; 522 int hintId = mSearchable.getHintId(); 523 if (hintId != 0) { 524 hint = getContext().getString(hintId); 525 } 526 if (hint != null) { 527 mQueryTextView.setHint(hint); 528 } 529 } 530 } 531 532 /** 533 * Updates the auto-complete text view. 534 */ 535 private void updateSearchAutoComplete() { 536 // close any existing suggestions adapter 537 //closeSuggestionsAdapter(); 538 539 mQueryTextView.setDropDownAnimationStyle(0); // no animation 540 541 // attach the suggestions adapter, if suggestions are available 542 // The existence of a suggestions authority is the proxy for "suggestions available here" 543 if (mSearchable.getSuggestAuthority() != null) { 544 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), 545 this, mSearchable, mOutsideDrawablesCache); 546 mQueryTextView.setAdapter(mSuggestionsAdapter); 547 ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement( 548 mQueryRefinement ? SuggestionsAdapter.REFINE_ALL 549 : SuggestionsAdapter.REFINE_BY_ENTRY); 550 } 551 } 552 553 /** 554 * Update the visibility of the voice button. There are actually two voice search modes, 555 * either of which will activate the button. 556 * @param empty whether the search query text field is empty. If it is, then the other 557 * criteria apply to make the voice button visible. Otherwise the voice button will not 558 * be visible - i.e., if the user has typed a query, remove the voice button. 559 */ 560 private void updateVoiceButton(boolean empty) { 561 int visibility = View.GONE; 562 if (mSearchable != null && mSearchable.getVoiceSearchEnabled() && empty 563 && !isIconified()) { 564 Intent testIntent = null; 565 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 566 testIntent = mVoiceWebSearchIntent; 567 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 568 testIntent = mVoiceAppSearchIntent; 569 } 570 if (testIntent != null) { 571 ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent, 572 PackageManager.MATCH_DEFAULT_ONLY); 573 if (ri != null) { 574 visibility = View.VISIBLE; 575 } 576 } 577 } 578 mVoiceButton.setVisibility(visibility); 579 } 580 581 private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() { 582 583 /** 584 * Called when the input method default action key is pressed. 585 */ 586 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 587 onSubmitQuery(); 588 return true; 589 } 590 }; 591 592 private void onTextChanged(CharSequence newText) { 593 CharSequence text = mQueryTextView.getText(); 594 boolean hasText = !TextUtils.isEmpty(text); 595 if (isSubmitButtonEnabled()) { 596 mSubmitButton.setVisibility(hasText ? VISIBLE : GONE); 597 } 598 updateVoiceButton(!hasText); 599 if (mOnQueryChangeListener != null) { 600 mOnQueryChangeListener.onQueryTextChanged(newText.toString()); 601 } 602 } 603 604 private void onSubmitQuery() { 605 CharSequence query = mQueryTextView.getText(); 606 if (!TextUtils.isEmpty(query)) { 607 if (mOnQueryChangeListener == null 608 || !mOnQueryChangeListener.onSubmitQuery(query.toString())) { 609 if (mSearchable != null) { 610 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString()); 611 setImeVisibility(false); 612 } 613 dismissSuggestions(); 614 } 615 } 616 } 617 618 private void dismissSuggestions() { 619 mQueryTextView.dismissDropDown(); 620 } 621 622 private void onCloseClicked() { 623 if (mOnCloseListener == null || !mOnCloseListener.onClose()) { 624 CharSequence text = mQueryTextView.getText(); 625 if (TextUtils.isEmpty(text)) { 626 // query field already empty, hide the keyboard and remove focus 627 clearFocus(); 628 setImeVisibility(false); 629 } else { 630 mQueryTextView.setText(""); 631 } 632 updateViewsVisibility(mIconifiedByDefault); 633 if (mIconifiedByDefault) setImeVisibility(false); 634 } 635 } 636 637 private void onSearchClicked() { 638 mQueryTextView.requestFocus(); 639 updateViewsVisibility(false); 640 setImeVisibility(true); 641 } 642 643 private void onVoiceClicked() { 644 // guard against possible race conditions 645 if (mSearchable == null) { 646 return; 647 } 648 SearchableInfo searchable = mSearchable; 649 try { 650 if (searchable.getVoiceSearchLaunchWebSearch()) { 651 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, 652 searchable); 653 getContext().startActivity(webSearchIntent); 654 } else if (searchable.getVoiceSearchLaunchRecognizer()) { 655 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, 656 searchable); 657 getContext().startActivity(appSearchIntent); 658 } 659 } catch (ActivityNotFoundException e) { 660 // Should not happen, since we check the availability of 661 // voice search before showing the button. But just in case... 662 Log.w(LOG_TAG, "Could not find voice search activity"); 663 } 664 } 665 666 private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { 667 668 /** 669 * Implements OnItemClickListener 670 */ 671 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 672 if (DBG) 673 Log.d(LOG_TAG, "onItemClick() position " + position); 674 if (mOnSuggestionListener == null 675 || !mOnSuggestionListener.onSuggestionClicked(position)) { 676 launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); 677 dismissSuggestions(); 678 } 679 } 680 }; 681 682 private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() { 683 684 /** 685 * Implements OnItemSelectedListener 686 */ 687 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 688 if (DBG) 689 Log.d(LOG_TAG, "onItemSelected() position " + position); 690 // A suggestion has been selected, rewrite the query if possible, 691 // otherwise the restore the original query. 692 if (mOnSuggestionListener == null 693 || !mOnSuggestionListener.onSuggestionSelected(position)) { 694 rewriteQueryFromSuggestion(position); 695 } 696 } 697 698 /** 699 * Implements OnItemSelectedListener 700 */ 701 public void onNothingSelected(AdapterView<?> parent) { 702 if (DBG) 703 Log.d(LOG_TAG, "onNothingSelected()"); 704 } 705 }; 706 707 /** 708 * Query rewriting. 709 */ 710 private void rewriteQueryFromSuggestion(int position) { 711 CharSequence oldQuery = mQueryTextView.getText(); 712 Cursor c = mSuggestionsAdapter.getCursor(); 713 if (c == null) { 714 return; 715 } 716 if (c.moveToPosition(position)) { 717 // Get the new query from the suggestion. 718 CharSequence newQuery = mSuggestionsAdapter.convertToString(c); 719 if (newQuery != null) { 720 // The suggestion rewrites the query. 721 // Update the text field, without getting new suggestions. 722 setQuery(newQuery); 723 } else { 724 // The suggestion does not rewrite the query, restore the user's query. 725 setQuery(oldQuery); 726 } 727 } else { 728 // We got a bad position, restore the user's query. 729 setQuery(oldQuery); 730 } 731 } 732 733 /** 734 * Launches an intent based on a suggestion. 735 * 736 * @param position The index of the suggestion to create the intent from. 737 * @param actionKey The key code of the action key that was pressed, 738 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 739 * @param actionMsg The message for the action key that was pressed, 740 * or <code>null</code> if none. 741 * @return true if a successful launch, false if could not (e.g. bad position). 742 */ 743 private boolean launchSuggestion(int position, int actionKey, String actionMsg) { 744 Cursor c = mSuggestionsAdapter.getCursor(); 745 if ((c != null) && c.moveToPosition(position)) { 746 747 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); 748 749 // launch the intent 750 launchIntent(intent); 751 752 return true; 753 } 754 return false; 755 } 756 757 /** 758 * Launches an intent, including any special intent handling. 759 */ 760 private void launchIntent(Intent intent) { 761 if (intent == null) { 762 return; 763 } 764 try { 765 // If the intent was created from a suggestion, it will always have an explicit 766 // component here. 767 getContext().startActivity(intent); 768 } catch (RuntimeException ex) { 769 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 770 } 771 } 772 773 /** 774 * Sets the text in the query box, without updating the suggestions. 775 */ 776 private void setQuery(CharSequence query) { 777 mQueryTextView.setText(query, true); 778 // Move the cursor to the end 779 mQueryTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length()); 780 } 781 782 private void launchQuerySearch(int actionKey, String actionMsg, String query) { 783 String action = Intent.ACTION_SEARCH; 784 Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); 785 getContext().startActivity(intent); 786 } 787 788 /** 789 * Constructs an intent from the given information and the search dialog state. 790 * 791 * @param action Intent action. 792 * @param data Intent data, or <code>null</code>. 793 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 794 * @param query Intent query, or <code>null</code>. 795 * @param actionKey The key code of the action key that was pressed, 796 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 797 * @param actionMsg The message for the action key that was pressed, 798 * or <code>null</code> if none. 799 * @param mode The search mode, one of the acceptable values for 800 * {@link SearchManager#SEARCH_MODE}, or {@code null}. 801 * @return The intent. 802 */ 803 private Intent createIntent(String action, Uri data, String extraData, String query, 804 int actionKey, String actionMsg) { 805 // Now build the Intent 806 Intent intent = new Intent(action); 807 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 808 // We need CLEAR_TOP to avoid reusing an old task that has other activities 809 // on top of the one we want. We don't want to do this in in-app search though, 810 // as it can be destructive to the activity stack. 811 if (data != null) { 812 intent.setData(data); 813 } 814 intent.putExtra(SearchManager.USER_QUERY, query); 815 if (query != null) { 816 intent.putExtra(SearchManager.QUERY, query); 817 } 818 if (extraData != null) { 819 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 820 } 821 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 822 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 823 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 824 } 825 intent.setComponent(mSearchable.getSearchActivity()); 826 return intent; 827 } 828 829 /** 830 * Create and return an Intent that can launch the voice search activity for web search. 831 */ 832 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { 833 Intent voiceIntent = new Intent(baseIntent); 834 ComponentName searchActivity = searchable.getSearchActivity(); 835 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null 836 : searchActivity.flattenToShortString()); 837 return voiceIntent; 838 } 839 840 /** 841 * Create and return an Intent that can launch the voice search activity, perform a specific 842 * voice transcription, and forward the results to the searchable activity. 843 * 844 * @param baseIntent The voice app search intent to start from 845 * @return A completely-configured intent ready to send to the voice search activity 846 */ 847 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { 848 ComponentName searchActivity = searchable.getSearchActivity(); 849 850 // create the necessary intent to set up a search-and-forward operation 851 // in the voice search system. We have to keep the bundle separate, 852 // because it becomes immutable once it enters the PendingIntent 853 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 854 queryIntent.setComponent(searchActivity); 855 PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent, 856 PendingIntent.FLAG_ONE_SHOT); 857 858 // Now set up the bundle that will be inserted into the pending intent 859 // when it's time to do the search. We always build it here (even if empty) 860 // because the voice search activity will always need to insert "QUERY" into 861 // it anyway. 862 Bundle queryExtras = new Bundle(); 863 864 // Now build the intent to launch the voice search. Add all necessary 865 // extras to launch the voice recognizer, and then all the necessary extras 866 // to forward the results to the searchable activity 867 Intent voiceIntent = new Intent(baseIntent); 868 869 // Add all of the configuration options supplied by the searchable's metadata 870 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 871 String prompt = null; 872 String language = null; 873 int maxResults = 1; 874 875 Resources resources = getResources(); 876 if (searchable.getVoiceLanguageModeId() != 0) { 877 languageModel = resources.getString(searchable.getVoiceLanguageModeId()); 878 } 879 if (searchable.getVoicePromptTextId() != 0) { 880 prompt = resources.getString(searchable.getVoicePromptTextId()); 881 } 882 if (searchable.getVoiceLanguageId() != 0) { 883 language = resources.getString(searchable.getVoiceLanguageId()); 884 } 885 if (searchable.getVoiceMaxResults() != 0) { 886 maxResults = searchable.getVoiceMaxResults(); 887 } 888 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 889 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 890 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 891 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 892 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null 893 : searchActivity.flattenToShortString()); 894 895 // Add the values that configure forwarding the results 896 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 897 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 898 899 return voiceIntent; 900 } 901 902 /** 903 * When a particular suggestion has been selected, perform the various lookups required 904 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 905 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 906 * the suggestion includes a data id. 907 * 908 * @param c The suggestions cursor, moved to the row of the user's selection 909 * @param actionKey The key code of the action key that was pressed, 910 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 911 * @param actionMsg The message for the action key that was pressed, 912 * or <code>null</code> if none. 913 * @return An intent for the suggestion at the cursor's position. 914 */ 915 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { 916 try { 917 // use specific action if supplied, or default action if supplied, or fixed default 918 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 919 920 if (action == null) { 921 action = mSearchable.getSuggestIntentAction(); 922 } 923 if (action == null) { 924 action = Intent.ACTION_SEARCH; 925 } 926 927 // use specific data if supplied, or default data if supplied 928 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 929 if (data == null) { 930 data = mSearchable.getSuggestIntentData(); 931 } 932 // then, if an ID was provided, append it. 933 if (data != null) { 934 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 935 if (id != null) { 936 data = data + "/" + Uri.encode(id); 937 } 938 } 939 Uri dataUri = (data == null) ? null : Uri.parse(data); 940 941 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); 942 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 943 944 return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); 945 } catch (RuntimeException e ) { 946 int rowNum; 947 try { // be really paranoid now 948 rowNum = c.getPosition(); 949 } catch (RuntimeException e2 ) { 950 rowNum = -1; 951 } 952 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 953 " returned exception" + e.toString()); 954 return null; 955 } 956 } 957 958 /** 959 * Callback to watch the text field for empty/non-empty 960 */ 961 private TextWatcher mTextWatcher = new TextWatcher() { 962 963 public void beforeTextChanged(CharSequence s, int start, int before, int after) { } 964 965 public void onTextChanged(CharSequence s, int start, 966 int before, int after) { 967 SearchView.this.onTextChanged(s); 968 } 969 970 public void afterTextChanged(Editable s) { 971 } 972 }; 973} 974