SearchDialog.java revision a8f556ee8cee24674663fd73c7a5b5a919b2d5bb
1/* 2 * Copyright (C) 2008 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.app; 18 19import android.content.ActivityNotFoundException; 20import android.content.BroadcastReceiver; 21import android.content.ComponentName; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.content.pm.PackageManager; 26import android.content.pm.ResolveInfo; 27import android.content.res.Configuration; 28import android.content.res.Resources; 29import android.content.res.Resources.NotFoundException; 30import android.database.Cursor; 31import android.graphics.drawable.Drawable; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.Handler; 35import android.os.RemoteException; 36import android.os.ServiceManager; 37import android.os.SystemClock; 38import android.server.search.SearchableInfo; 39import android.speech.RecognizerIntent; 40import android.text.Editable; 41import android.text.InputType; 42import android.text.TextUtils; 43import android.text.TextWatcher; 44import android.util.AttributeSet; 45import android.util.Log; 46import android.view.Gravity; 47import android.view.KeyEvent; 48import android.view.View; 49import android.view.ViewGroup; 50import android.view.Window; 51import android.view.WindowManager; 52import android.view.inputmethod.InputMethodManager; 53import android.widget.AdapterView; 54import android.widget.AutoCompleteTextView; 55import android.widget.Button; 56import android.widget.CursorAdapter; 57import android.widget.ImageButton; 58import android.widget.ImageView; 59import android.widget.ListView; 60import android.widget.SimpleCursorAdapter; 61import android.widget.TextView; 62import android.widget.WrapperListAdapter; 63import android.widget.AdapterView.OnItemClickListener; 64import android.widget.AdapterView.OnItemSelectedListener; 65 66import java.lang.ref.WeakReference; 67import java.util.concurrent.atomic.AtomicLong; 68 69/** 70 * In-application-process implementation of Search Bar. This is still controlled by the 71 * SearchManager, but it runs in the current activity's process to keep things lighter weight. 72 * 73 * @hide 74 */ 75public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener { 76 77 // Debugging support 78 final static String LOG_TAG = "SearchDialog"; 79 private static final int DBG_LOG_TIMING = 0; 80 final static int DBG_JAM_THREADING = 0; 81 82 // interaction with runtime 83 IntentFilter mCloseDialogsFilter; 84 IntentFilter mPackageFilter; 85 86 private static final String INSTANCE_KEY_COMPONENT = "comp"; 87 private static final String INSTANCE_KEY_APPDATA = "data"; 88 private static final String INSTANCE_KEY_GLOBALSEARCH = "glob"; 89 private static final String INSTANCE_KEY_DISPLAY_QUERY = "dQry"; 90 private static final String INSTANCE_KEY_DISPLAY_SEL_START = "sel1"; 91 private static final String INSTANCE_KEY_DISPLAY_SEL_END = "sel2"; 92 private static final String INSTANCE_KEY_USER_QUERY = "uQry"; 93 private static final String INSTANCE_KEY_SUGGESTION_QUERY = "sQry"; 94 private static final String INSTANCE_KEY_SELECTED_ELEMENT = "slEl"; 95 private static final int INSTANCE_SELECTED_BUTTON = -2; 96 private static final int INSTANCE_SELECTED_QUERY = -1; 97 98 // views & widgets 99 private TextView mBadgeLabel; 100 private AutoCompleteTextView mSearchTextField; 101 private Button mGoButton; 102 private ImageButton mVoiceButton; 103 104 // interaction with searchable application 105 private ComponentName mLaunchComponent; 106 private Bundle mAppSearchData; 107 private boolean mGlobalSearchMode; 108 private Context mActivityContext; 109 110 // interaction with the search manager service 111 private SearchableInfo mSearchable; 112 113 // support for suggestions 114 private String mUserQuery = null; 115 private int mUserQuerySelStart; 116 private int mUserQuerySelEnd; 117 private boolean mLeaveJammedQueryOnRefocus = false; 118 private String mPreviousSuggestionQuery = null; 119 private int mPresetSelection = -1; 120 private String mSuggestionAction = null; 121 private Uri mSuggestionData = null; 122 private String mSuggestionQuery = null; 123 124 // For voice searching 125 private Intent mVoiceWebSearchIntent; 126 private Intent mVoiceAppSearchIntent; 127 128 // support for AutoCompleteTextView suggestions display 129 private SuggestionsAdapter mSuggestionsAdapter; 130 131 /** 132 * Constructor - fires it up and makes it look like the search UI. 133 * 134 * @param context Application Context we can use for system acess 135 */ 136 public SearchDialog(Context context) { 137 super(context, com.android.internal.R.style.Theme_SearchBar); 138 } 139 140 /** 141 * We create the search dialog just once, and it stays around (hidden) 142 * until activated by the user. 143 */ 144 @Override 145 protected void onCreate(Bundle savedInstanceState) { 146 super.onCreate(savedInstanceState); 147 148 Window theWindow = getWindow(); 149 theWindow.setGravity(Gravity.TOP|Gravity.FILL_HORIZONTAL); 150 151 setContentView(com.android.internal.R.layout.search_bar); 152 153 theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT, 154 ViewGroup.LayoutParams.WRAP_CONTENT); 155 WindowManager.LayoutParams lp = theWindow.getAttributes(); 156 lp.setTitle("Search Dialog"); 157 lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; 158 theWindow.setAttributes(lp); 159 160 // get the view elements for local access 161 mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge); 162 mSearchTextField = (AutoCompleteTextView) 163 findViewById(com.android.internal.R.id.search_src_text); 164 mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn); 165 mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn); 166 167 // attach listeners 168 mSearchTextField.addTextChangedListener(mTextWatcher); 169 mSearchTextField.setOnKeyListener(mTextKeyListener); 170 mGoButton.setOnClickListener(mGoButtonClickListener); 171 mGoButton.setOnKeyListener(mButtonsKeyListener); 172 mVoiceButton.setOnClickListener(mVoiceButtonClickListener); 173 mVoiceButton.setOnKeyListener(mButtonsKeyListener); 174 175 // pre-hide all the extraneous elements 176 mBadgeLabel.setVisibility(View.GONE); 177 178 // Additional adjustments to make Dialog work for Search 179 180 // Touching outside of the search dialog will dismiss it 181 setCanceledOnTouchOutside(true); 182 183 // Set up broadcast filters 184 mCloseDialogsFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 185 mPackageFilter = new IntentFilter(); 186 mPackageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 187 mPackageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 188 mPackageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 189 mPackageFilter.addDataScheme("package"); 190 191 // Save voice intent for later queries/launching 192 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); 193 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 194 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 195 196 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 197 } 198 199 /** 200 * Set up the search dialog 201 * 202 * @param Returns true if search dialog launched, false if not 203 */ 204 public boolean show(String initialQuery, boolean selectInitialQuery, 205 ComponentName componentName, Bundle appSearchData, boolean globalSearch) { 206 if (isShowing()) { 207 // race condition - already showing but not handling events yet. 208 // in this case, just discard the "show" request 209 return true; 210 } 211 212 // Get searchable info from search manager and use to set up other elements of UI 213 // Do this first so we can get out quickly if there's nothing to search 214 ISearchManager sms; 215 sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE)); 216 try { 217 mSearchable = sms.getSearchableInfo(componentName, globalSearch); 218 } catch (RemoteException e) { 219 mSearchable = null; 220 } 221 if (mSearchable == null) { 222 // unfortunately, we can't log here. it would be logspam every time the user 223 // clicks the "search" key on a non-search app 224 return false; 225 } 226 227 // OK, we're going to show ourselves 228 super.show(); 229 230 setupSearchableInfo(); 231 232 mLaunchComponent = componentName; 233 mAppSearchData = appSearchData; 234 mGlobalSearchMode = globalSearch; 235 236 // receive broadcasts 237 getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter); 238 getContext().registerReceiver(mBroadcastReceiver, mPackageFilter); 239 240 // configure the autocomplete aspects of the input box 241 mSearchTextField.setOnItemClickListener(this); 242 mSearchTextField.setOnItemSelectedListener(this); 243 244 // This conversion is necessary to force a preload of the EditText and thus force 245 // suggestions to be presented (even for an empty query) 246 if (initialQuery == null) { 247 initialQuery = ""; // This forces the preload to happen, triggering suggestions 248 } 249 250 // attach the suggestions adapter, if suggestions are available 251 // The existence of a suggestions authority is the proxy for "suggestions available here" 252 if (mSearchable.getSuggestAuthority() == null) { 253 mSuggestionsAdapter = null; 254 mSearchTextField.setAdapter(mSuggestionsAdapter); 255 mSearchTextField.setText(initialQuery); 256 } else { 257 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable, 258 mSearchTextField); 259 mSearchTextField.setAdapter(mSuggestionsAdapter); 260 261 // finally, load the user's initial text (which may trigger suggestions) 262 mSuggestionsAdapter.setNonUserQuery(false); 263 mSearchTextField.setText(initialQuery); 264 } 265 266 if (selectInitialQuery) { 267 mSearchTextField.selectAll(); 268 } else { 269 mSearchTextField.setSelection(initialQuery.length()); 270 } 271 return true; 272 } 273 274 /** 275 * The default show() for this Dialog is not supported. 276 */ 277 @Override 278 public void show() { 279 return; 280 } 281 282 /** 283 * The search dialog is being dismissed, so handle all of the local shutdown operations. 284 * 285 * This function is designed to be idempotent so that dismiss() can be safely called at any time 286 * (even if already closed) and more likely to really dump any memory. No leaks! 287 */ 288 @Override 289 public void onStop() { 290 super.onStop(); 291 292 setOnCancelListener(null); 293 setOnDismissListener(null); 294 295 // stop receiving broadcasts (throws exception if none registered) 296 try { 297 getContext().unregisterReceiver(mBroadcastReceiver); 298 } catch (RuntimeException e) { 299 // This is OK - it just means we didn't have any registered 300 } 301 302 // close any leftover cursor 303 if (mSuggestionsAdapter != null) { 304 mSuggestionsAdapter.changeCursor(null); 305 } 306 307 // dump extra memory we're hanging on to 308 mLaunchComponent = null; 309 mAppSearchData = null; 310 mSearchable = null; 311 mSuggestionAction = null; 312 mSuggestionData = null; 313 mSuggestionQuery = null; 314 mActivityContext = null; 315 mPreviousSuggestionQuery = null; 316 mUserQuery = null; 317 } 318 319 /** 320 * Save the minimal set of data necessary to recreate the search 321 * 322 * @return A bundle with the state of the dialog. 323 */ 324 @Override 325 public Bundle onSaveInstanceState() { 326 Bundle bundle = new Bundle(); 327 328 // setup info so I can recreate this particular search 329 bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent); 330 bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData); 331 bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode); 332 333 // UI state 334 bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchTextField.getText().toString()); 335 bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchTextField.getSelectionStart()); 336 bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchTextField.getSelectionEnd()); 337 bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 338 bundle.putString(INSTANCE_KEY_SUGGESTION_QUERY, mPreviousSuggestionQuery); 339 340 int selectedElement = INSTANCE_SELECTED_QUERY; 341 if (mGoButton.isFocused()) { 342 selectedElement = INSTANCE_SELECTED_BUTTON; 343 } else if (mSearchTextField.isPopupShowing()) { 344 selectedElement = 0; // TODO mSearchTextField.getListSelection() // 0..n 345 } 346 bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement); 347 348 return bundle; 349 } 350 351 /** 352 * Restore the state of the dialog from a previously saved bundle. 353 * 354 * @param savedInstanceState The state of the dialog previously saved by 355 * {@link #onSaveInstanceState()}. 356 */ 357 @Override 358 public void onRestoreInstanceState(Bundle savedInstanceState) { 359 // Get the launch info 360 ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT); 361 Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA); 362 boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH); 363 364 // get the UI state 365 String displayQuery = savedInstanceState.getString(INSTANCE_KEY_DISPLAY_QUERY); 366 int querySelStart = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_START, -1); 367 int querySelEnd = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_END, -1); 368 String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 369 int selectedElement = savedInstanceState.getInt(INSTANCE_KEY_SELECTED_ELEMENT); 370 String suggestionQuery = savedInstanceState.getString(INSTANCE_KEY_SUGGESTION_QUERY); 371 372 // show the dialog. skip any show/hide animation, we want to go fast. 373 // send the text that actually generates the suggestions here; we'll replace the display 374 // text as necessary in a moment. 375 if (!show(suggestionQuery, false, launchComponent, appSearchData, globalSearch)) { 376 // for some reason, we couldn't re-instantiate 377 return; 378 } 379 380 if (mSuggestionsAdapter != null) { 381 mSuggestionsAdapter.setNonUserQuery(true); 382 } 383 mSearchTextField.setText(displayQuery); 384 // TODO because the new query is (not) processed in another thread, we can't just 385 // take away this flag (yet). The better solution here is going to require a new API 386 // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions. 387// mSuggestionsAdapter.setNonUserQuery(false); 388 389 // clean up the selection state 390 switch (selectedElement) { 391 case INSTANCE_SELECTED_BUTTON: 392 mGoButton.setEnabled(true); 393 mGoButton.setFocusable(true); 394 mGoButton.requestFocus(); 395 break; 396 case INSTANCE_SELECTED_QUERY: 397 if (querySelStart >= 0 && querySelEnd >= 0) { 398 mSearchTextField.requestFocus(); 399 mSearchTextField.setSelection(querySelStart, querySelEnd); 400 } 401 break; 402 default: 403 // defer selecting a list element until suggestion list appears 404 mPresetSelection = selectedElement; 405 // TODO mSearchTextField.setListSelection(selectedElement) 406 break; 407 } 408 } 409 410 /** 411 * Hook for updating layout on a rotation 412 * 413 */ 414 public void onConfigurationChanged(Configuration newConfig) { 415 if (isShowing()) { 416 // Redraw (resources may have changed) 417 updateSearchButton(); 418 updateSearchBadge(); 419 updateQueryHint(); 420 } 421 } 422 423 /** 424 * Use SearchableInfo record (from search manager service) to preconfigure the UI in various 425 * ways. 426 */ 427 private void setupSearchableInfo() { 428 if (mSearchable != null) { 429 mActivityContext = mSearchable.getActivityContext(getContext()); 430 431 updateSearchButton(); 432 updateSearchBadge(); 433 updateQueryHint(); 434 updateVoiceButton(); 435 436 // In order to properly configure the input method (if one is being used), we 437 // need to let it know if we'll be providing suggestions. Although it would be 438 // difficult/expensive to know if every last detail has been configured properly, we 439 // can at least see if a suggestions provider has been configured, and use that 440 // as our trigger. 441 int inputType = mSearchable.getInputType(); 442 // We only touch this if the input type is set up for text (which it almost certainly 443 // should be, in the case of search!) 444 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { 445 // The existence of a suggestions authority is the proxy for "suggestions 446 // are available here" 447 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 448 if (mSearchable.getSuggestAuthority() != null) { 449 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 450 } 451 } 452 mSearchTextField.setInputType(inputType); 453 mSearchTextField.setImeOptions(mSearchable.getImeOptions()); 454 } 455 } 456 457 /** 458 * The list of installed packages has just changed. This means that our current context 459 * may no longer be valid. This would only happen if a package is installed/removed exactly 460 * when the search bar is open. So for now we're just going to close the search 461 * bar. 462 * 463 * Anything fancier would require some checks to see if the user's context was still valid. 464 * Which would be messier. 465 */ 466 public void onPackageListChange() { 467 cancel(); 468 } 469 470 /** 471 * Update the text in the search button. Note: This is deprecated functionality, for 472 * 1.0 compatibility only. 473 */ 474 private void updateSearchButton() { 475 String textLabel = null; 476 Drawable iconLabel = null; 477 int textId = mSearchable.getSearchButtonText(); 478 if (textId != 0) { 479 textLabel = mActivityContext.getResources().getString(textId); 480 } else { 481 iconLabel = getContext().getResources(). 482 getDrawable(com.android.internal.R.drawable.ic_btn_search); 483 } 484 mGoButton.setText(textLabel); 485 mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null); 486 } 487 488 /** 489 * Setup the search "Badge" if request by mode flags. 490 */ 491 private void updateSearchBadge() { 492 // assume both hidden 493 int visibility = View.GONE; 494 Drawable icon = null; 495 String text = null; 496 497 // optionally show one or the other. 498 if (mSearchable.mBadgeIcon) { 499 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId()); 500 visibility = View.VISIBLE; 501 } else if (mSearchable.mBadgeLabel) { 502 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString(); 503 visibility = View.VISIBLE; 504 } 505 506 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); 507 mBadgeLabel.setText(text); 508 mBadgeLabel.setVisibility(visibility); 509 } 510 511 /** 512 * Update the hint in the query text field. 513 */ 514 private void updateQueryHint() { 515 if (isShowing()) { 516 String hint = null; 517 if (mSearchable != null) { 518 int hintId = mSearchable.getHintId(); 519 if (hintId != 0) { 520 hint = mActivityContext.getString(hintId); 521 } 522 } 523 mSearchTextField.setHint(hint); 524 } 525 } 526 527 /** 528 * Update the visibility of the voice button. There are actually two voice search modes, 529 * either of which will activate the button. 530 */ 531 private void updateVoiceButton() { 532 int visibility = View.GONE; 533 if (mSearchable.getVoiceSearchEnabled()) { 534 Intent testIntent = null; 535 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 536 testIntent = mVoiceWebSearchIntent; 537 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 538 testIntent = mVoiceAppSearchIntent; 539 } 540 if (testIntent != null) { 541 ResolveInfo ri = getContext().getPackageManager(). 542 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); 543 if (ri != null) { 544 visibility = View.VISIBLE; 545 } 546 } 547 } 548 mVoiceButton.setVisibility(visibility); 549 } 550 551 /** 552 * Listeners of various types 553 */ 554 555 /** 556 * Dialog's OnKeyListener implements various search-specific functionality 557 * 558 * @param keyCode This is the keycode of the typed key, and is the same value as 559 * found in the KeyEvent parameter. 560 * @param event The complete event record for the typed key 561 * 562 * @return Return true if the event was handled here, or false if not. 563 */ 564 @Override 565 public boolean onKeyDown(int keyCode, KeyEvent event) { 566 switch (keyCode) { 567 case KeyEvent.KEYCODE_BACK: 568 cancel(); 569 return true; 570 case KeyEvent.KEYCODE_SEARCH: 571 if (TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0) { 572 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 573 } else { 574 cancel(); 575 } 576 return true; 577 default: 578 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 579 if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) { 580 launchQuerySearch(keyCode, actionKey.mQueryActionMsg); 581 return true; 582 } 583 break; 584 } 585 return false; 586 } 587 588 /** 589 * Callback to watch the textedit field for empty/non-empty 590 */ 591 private TextWatcher mTextWatcher = new TextWatcher() { 592 593 public void beforeTextChanged(CharSequence s, int start, int 594 before, int after) { } 595 596 public void onTextChanged(CharSequence s, int start, 597 int before, int after) { 598 if (DBG_LOG_TIMING == 1) { 599 dbgLogTiming("onTextChanged()"); 600 } 601 updateWidgetState(); 602 // Only do suggestions if actually typed by user 603 if ((mSuggestionsAdapter != null) && !mSuggestionsAdapter.getNonUserQuery()) { 604 mPreviousSuggestionQuery = s.toString(); 605 mUserQuery = mSearchTextField.getText().toString(); 606 mUserQuerySelStart = mSearchTextField.getSelectionStart(); 607 mUserQuerySelEnd = mSearchTextField.getSelectionEnd(); 608 } 609 } 610 611 public void afterTextChanged(Editable s) { } 612 }; 613 614 /** 615 * Enable/Disable the cancel button based on edit text state (any text?) 616 */ 617 private void updateWidgetState() { 618 // enable the button if we have one or more non-space characters 619 boolean enabled = 620 TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0; 621 622 mGoButton.setEnabled(enabled); 623 mGoButton.setFocusable(enabled); 624 } 625 626 private final static String[] ONE_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1 }; 627 private final static String[] ONE_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1, 628 SearchManager.SUGGEST_COLUMN_ICON_1, 629 SearchManager.SUGGEST_COLUMN_ICON_2}; 630 private final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1, 631 SearchManager.SUGGEST_COLUMN_TEXT_2 }; 632 private final static String[] TWO_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1, 633 SearchManager.SUGGEST_COLUMN_TEXT_2, 634 SearchManager.SUGGEST_COLUMN_ICON_1, 635 SearchManager.SUGGEST_COLUMN_ICON_2 }; 636 637 private final static int[] ONE_LINE_TO = {com.android.internal.R.id.text1}; 638 private final static int[] ONE_LINE_ICONS_TO = {com.android.internal.R.id.text1, 639 com.android.internal.R.id.icon1, 640 com.android.internal.R.id.icon2}; 641 private final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1, 642 com.android.internal.R.id.text2}; 643 private final static int[] TWO_LINE_ICONS_TO = {com.android.internal.R.id.text1, 644 com.android.internal.R.id.text2, 645 com.android.internal.R.id.icon1, 646 com.android.internal.R.id.icon2}; 647 648 /** 649 * Safely retrieve the suggestions cursor adapter from the ListView 650 * 651 * @param adapterView The ListView containing our adapter 652 * @result The CursorAdapter that we installed, or null if not set 653 */ 654 private static CursorAdapter getSuggestionsAdapter(AdapterView<?> adapterView) { 655 CursorAdapter result = null; 656 if (adapterView != null) { 657 Object ad = adapterView.getAdapter(); 658 if (ad instanceof CursorAdapter) { 659 result = (CursorAdapter) ad; 660 } else if (ad instanceof WrapperListAdapter) { 661 result = (CursorAdapter) ((WrapperListAdapter)ad).getWrappedAdapter(); 662 } 663 } 664 return result; 665 } 666 667 /** 668 * React to typing in the GO search button by refocusing to EditText. 669 * Continue typing the query. 670 */ 671 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() { 672 public boolean onKey(View v, int keyCode, KeyEvent event) { 673 // also guard against possible race conditions (late arrival after dismiss) 674 if (mSearchable != null) { 675 return refocusingKeyListener(v, keyCode, event); 676 } 677 return false; 678 } 679 }; 680 681 /** 682 * React to a click in the GO button by launching a search. 683 */ 684 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() { 685 public void onClick(View v) { 686 // also guard against possible race conditions (late arrival after dismiss) 687 if (mSearchable != null) { 688 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 689 } 690 } 691 }; 692 693 /** 694 * React to a click in the voice search button. 695 */ 696 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() { 697 public void onClick(View v) { 698 try { 699 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 700 getContext().startActivity(mVoiceWebSearchIntent); 701 dismiss(); 702 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 703 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent); 704 getContext().startActivity(appSearchIntent); 705 dismiss(); 706 } 707 } catch (ActivityNotFoundException e) { 708 // Should not happen, since we check the availability of 709 // voice search before showing the button. But just in case... 710 Log.w(LOG_TAG, "Could not find voice search activity"); 711 } 712 } 713 }; 714 715 /** 716 * Create and return an Intent that can launch the voice search activity, perform a specific 717 * voice transcription, and forward the results to the searchable activity. 718 * 719 * @param baseIntent The voice app search intent to start from 720 * @return A completely-configured intent ready to send to the voice search activity 721 */ 722 private Intent createVoiceAppSearchIntent(Intent baseIntent) { 723 // create the necessary intent to set up a search-and-forward operation 724 // in the voice search system. We have to keep the bundle separate, 725 // because it becomes immutable once it enters the PendingIntent 726 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 727 queryIntent.setComponent(mSearchable.mSearchActivity); 728 PendingIntent pending = PendingIntent.getActivity( 729 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT); 730 731 // Now set up the bundle that will be inserted into the pending intent 732 // when it's time to do the search. We always build it here (even if empty) 733 // because the voice search activity will always need to insert "QUERY" into 734 // it anyway. 735 Bundle queryExtras = new Bundle(); 736 if (mAppSearchData != null) { 737 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData); 738 } 739 740 // Now build the intent to launch the voice search. Add all necessary 741 // extras to launch the voice recognizer, and then all the necessary extras 742 // to forward the results to the searchable activity 743 Intent voiceIntent = new Intent(baseIntent); 744 745 // Add all of the configuration options supplied by the searchable's metadata 746 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 747 String prompt = null; 748 String language = null; 749 int maxResults = 1; 750 Resources resources = mActivityContext.getResources(); 751 if (mSearchable.getVoiceLanguageModeId() != 0) { 752 languageModel = resources.getString(mSearchable.getVoiceLanguageModeId()); 753 } 754 if (mSearchable.getVoicePromptTextId() != 0) { 755 prompt = resources.getString(mSearchable.getVoicePromptTextId()); 756 } 757 if (mSearchable.getVoiceLanguageId() != 0) { 758 language = resources.getString(mSearchable.getVoiceLanguageId()); 759 } 760 if (mSearchable.getVoiceMaxResults() != 0) { 761 maxResults = mSearchable.getVoiceMaxResults(); 762 } 763 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 764 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 765 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 766 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 767 768 // Add the values that configure forwarding the results 769 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 770 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 771 772 return voiceIntent; 773 } 774 775 /** 776 * React to the user typing "enter" or other hardwired keys while typing in the search box. 777 * This handles these special keys while the edit box has focus. 778 */ 779 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { 780 public boolean onKey(View v, int keyCode, KeyEvent event) { 781 if (keyCode == KeyEvent.KEYCODE_BACK) { 782 cancel(); 783 return true; 784 } 785 // also guard against possible race conditions (late arrival after dismiss) 786 if (mSearchable != null && 787 TextUtils.getTrimmedLength(mSearchTextField.getText()) > 0) { 788 if (DBG_LOG_TIMING == 1) { 789 dbgLogTiming("doTextKey()"); 790 } 791 // dispatch "typing in the list" first 792 if (mSearchTextField.isPopupShowing() && 793 mSearchTextField.getListSelection() != ListView.INVALID_POSITION) { 794 return onSuggestionsKey(v, keyCode, event); 795 } 796 // otherwise, dispatch an "edit view" key 797 switch (keyCode) { 798 case KeyEvent.KEYCODE_ENTER: 799 if (event.getAction() == KeyEvent.ACTION_UP) { 800 v.cancelLongPress(); 801 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 802 return true; 803 } 804 break; 805 case KeyEvent.KEYCODE_DPAD_DOWN: 806 // capture the EditText state, so we can restore the user entry later 807 mUserQuery = mSearchTextField.getText().toString(); 808 mUserQuerySelStart = mSearchTextField.getSelectionStart(); 809 mUserQuerySelEnd = mSearchTextField.getSelectionEnd(); 810 // pass through - we're just watching here 811 break; 812 default: 813 if (event.getAction() == KeyEvent.ACTION_DOWN) { 814 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 815 if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) { 816 launchQuerySearch(keyCode, actionKey.mQueryActionMsg); 817 return true; 818 } 819 } 820 break; 821 } 822 } 823 return false; 824 } 825 }; 826 827 /** 828 * React to the user typing while the suggestions are focused. First, check for action 829 * keys. If not handled, try refocusing regular characters into the EditText. In this case, 830 * replace the query text (start typing fresh text). 831 */ 832 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { 833 boolean handled = false; 834 // also guard against possible race conditions (late arrival after dismiss) 835 if (mSearchable != null) { 836 handled = doSuggestionsKey(v, keyCode, event); 837 } 838 return handled; 839 } 840 841 /** 842 * Per UI design, we're going to "steer" any typed keystrokes back into the EditText 843 * box, even if the user has navigated the focus to the dropdown or to the GO button. 844 * 845 * @param v The view into which the keystroke was typed 846 * @param keyCode keyCode of entered key 847 * @param event Full KeyEvent record of entered key 848 */ 849 private boolean refocusingKeyListener(View v, int keyCode, KeyEvent event) { 850 boolean handled = false; 851 852 if (!event.isSystem() && 853 (keyCode != KeyEvent.KEYCODE_DPAD_UP) && 854 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) && 855 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && 856 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && 857 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { 858 // restore focus and give key to EditText ... 859 // but don't replace the user's query 860 mLeaveJammedQueryOnRefocus = true; 861 if (mSearchTextField.requestFocus()) { 862 handled = mSearchTextField.dispatchKeyEvent(event); 863 } 864 mLeaveJammedQueryOnRefocus = false; 865 } 866 return handled; 867 } 868 869 /** 870 * Update query text based on transitions in and out of suggestions list. 871 */ 872 /* 873 * TODO - figure out if this logic is required for the autocomplete text view version 874 875 OnFocusChangeListener mSuggestFocusListener = new OnFocusChangeListener() { 876 public void onFocusChange(View v, boolean hasFocus) { 877 // also guard against possible race conditions (late arrival after dismiss) 878 if (mSearchable == null) { 879 return; 880 } 881 // Update query text based on navigation in to/out of the suggestions list 882 if (hasFocus) { 883 // Entering the list view - record selection point from user's query 884 mUserQuery = mSearchTextField.getText().toString(); 885 mUserQuerySelStart = mSearchTextField.getSelectionStart(); 886 mUserQuerySelEnd = mSearchTextField.getSelectionEnd(); 887 // then update the query to match the entered selection 888 jamSuggestionQuery(true, mSuggestionsList, 889 mSuggestionsList.getSelectedItemPosition()); 890 } else { 891 // Exiting the list view 892 893 if (mSuggestionsList.getSelectedItemPosition() < 0) { 894 // Direct exit - Leave new suggestion in place (do nothing) 895 } else { 896 // Navigation exit - restore user's query text 897 if (!mLeaveJammedQueryOnRefocus) { 898 jamSuggestionQuery(false, null, -1); 899 } 900 } 901 } 902 903 } 904 }; 905 */ 906 907 /** 908 * This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent. It's an indication that 909 * we should close ourselves immediately, in order to allow a higher-priority UI to take over 910 * (e.g. phone call received). 911 */ 912 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 913 @Override 914 public void onReceive(Context context, Intent intent) { 915 String action = intent.getAction(); 916 if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) { 917 cancel(); 918 } else if (Intent.ACTION_PACKAGE_ADDED.equals(action) 919 || Intent.ACTION_PACKAGE_REMOVED.equals(action) 920 || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { 921 onPackageListChange(); 922 } 923 } 924 }; 925 926 @Override 927 public void cancel() { 928 // We made sure the IME was displayed, so also make sure it is closed 929 // when we go away. 930 InputMethodManager imm = (InputMethodManager)getContext() 931 .getSystemService(Context.INPUT_METHOD_SERVICE); 932 if (imm != null) { 933 imm.hideSoftInputFromWindow( 934 getWindow().getDecorView().getWindowToken(), 0); 935 } 936 937 super.cancel(); 938 } 939 940 /** 941 * Various ways to launch searches 942 */ 943 944 /** 945 * React to the user clicking the "GO" button. Hide the UI and launch a search. 946 * 947 * @param actionKey Pass a keycode if the launch was triggered by an action key. Pass 948 * KeyEvent.KEYCODE_UNKNOWN for no actionKey code. 949 * @param actionMsg Pass the suggestion-provided message if the launch was triggered by an 950 * action key. Pass null for no actionKey message. 951 */ 952 private void launchQuerySearch(int actionKey, final String actionMsg) { 953 final String query = mSearchTextField.getText().toString(); 954 final Bundle appData = mAppSearchData; 955 final SearchableInfo si = mSearchable; // cache briefly (dismiss() nulls it) 956 dismiss(); 957 sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, actionKey, actionMsg, si); 958 } 959 960 /** 961 * React to the user typing an action key while in the suggestions list 962 */ 963 private boolean doSuggestionsKey(View v, int keyCode, KeyEvent event) { 964 // Exit early in case of race condition 965 if (mSuggestionsAdapter == null) { 966 return false; 967 } 968 if (event.getAction() == KeyEvent.ACTION_DOWN) { 969 if (DBG_LOG_TIMING == 1) { 970 dbgLogTiming("doSuggestionsKey()"); 971 } 972 973 // First, check for enter or search (both of which we'll treat as a "click") 974 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 975 int position = mSearchTextField.getListSelection(); 976 return launchSuggestion(mSuggestionsAdapter, position); 977 } 978 979 // Next, check for left/right moves, which we use to "return" the user to the edit view 980 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 981 // give "focus" to text editor, but don't restore the user's original query 982 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 983 0 : mSearchTextField.length(); 984 mSearchTextField.setSelection(selPoint); 985 mSearchTextField.setListSelection(0); 986 mSearchTextField.clearListSelection(); 987 return true; 988 } 989 990 // Next, check for an "up and out" move 991 if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchTextField.getListSelection()) { 992 jamSuggestionQuery(false, null, -1); 993 // let ACTV complete the move 994 return false; 995 } 996 997 // Next, check for an "action key" 998 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 999 if ((actionKey != null) && 1000 ((actionKey.mSuggestActionMsg != null) || 1001 (actionKey.mSuggestActionMsgColumn != null))) { 1002 // launch suggestion using action key column 1003 int position = mSearchTextField.getListSelection(); 1004 if (position >= 0) { 1005 Cursor c = mSuggestionsAdapter.getCursor(); 1006 if (c.moveToPosition(position)) { 1007 final String actionMsg = getActionKeyMessage(c, actionKey); 1008 if (actionMsg != null && (actionMsg.length() > 0)) { 1009 // shut down search bar and launch the activity 1010 // cache everything we need because dismiss releases mems 1011 setupSuggestionIntent(c, mSearchable); 1012 final String query = mSearchTextField.getText().toString(); 1013 final Bundle appData = mAppSearchData; 1014 SearchableInfo si = mSearchable; 1015 String suggestionAction = mSuggestionAction; 1016 Uri suggestionData = mSuggestionData; 1017 String suggestionQuery = mSuggestionQuery; 1018 dismiss(); 1019 sendLaunchIntent(suggestionAction, suggestionData, 1020 suggestionQuery, appData, 1021 keyCode, actionMsg, si); 1022 return true; 1023 } 1024 } 1025 } 1026 } 1027 } 1028 return false; 1029 } 1030 1031 /** 1032 * Set or reset the user query to follow the selections in the suggestions 1033 * 1034 * @param jamQuery True means to set the query, false means to reset it to the user's choice 1035 */ 1036 private void jamSuggestionQuery(boolean jamQuery, AdapterView<?> parent, int position) { 1037 // quick check against race conditions 1038 if (mSearchable == null) { 1039 return; 1040 } 1041 1042 mSuggestionsAdapter.setNonUserQuery(true); // disables any suggestions processing 1043 if (jamQuery) { 1044 CursorAdapter ca = getSuggestionsAdapter(parent); 1045 Cursor c = ca.getCursor(); 1046 if (c.moveToPosition(position)) { 1047 setupSuggestionIntent(c, mSearchable); 1048 String jamText = null; 1049 1050 // Simple heuristic for selecting text with which to rewrite the query. 1051 if (mSuggestionQuery != null) { 1052 jamText = mSuggestionQuery; 1053 } else if (mSearchable.mQueryRewriteFromData && (mSuggestionData != null)) { 1054 jamText = mSuggestionData.toString(); 1055 } else if (mSearchable.mQueryRewriteFromText) { 1056 try { 1057 int column = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1); 1058 jamText = c.getString(column); 1059 } catch (RuntimeException e) { 1060 // no work here, jamText is null 1061 } 1062 } 1063 if (jamText != null) { 1064 mSearchTextField.setText(jamText); 1065 /* mSearchTextField.selectAll(); */ // this didn't work anyway in the old UI 1066 // TODO this is only needed in the model where we have a selection in the ACTV 1067 // and in the dropdown at the same time. 1068 mSearchTextField.setSelection(jamText.length()); 1069 } 1070 } 1071 } else { 1072 // reset user query 1073 mSearchTextField.setText(mUserQuery); 1074 try { 1075 mSearchTextField.setSelection(mUserQuerySelStart, mUserQuerySelEnd); 1076 } catch (IndexOutOfBoundsException e) { 1077 // In case of error, just select all 1078 Log.e(LOG_TAG, "Caught IndexOutOfBoundsException while setting selection. " + 1079 "start=" + mUserQuerySelStart + " end=" + mUserQuerySelEnd + 1080 " text=\"" + mUserQuery + "\""); 1081 mSearchTextField.selectAll(); 1082 } 1083 } 1084 // TODO because the new query is (not) processed in another thread, we can't just 1085 // take away this flag (yet). The better solution here is going to require a new API 1086 // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions. 1087// mSuggestionsAdapter.setNonUserQuery(false); 1088 } 1089 1090 /** 1091 * Assemble a search intent and send it. 1092 * 1093 * @param action The intent to send, typically Intent.ACTION_SEARCH 1094 * @param data The data for the intent 1095 * @param query The user text entered (so far) 1096 * @param appData The app data bundle (if supplied) 1097 * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will 1098 * be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code. 1099 * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the 1100 * corresponding tag message will be sent here. Pass null for no actionKey message. 1101 * @param si Reference to the current SearchableInfo. Passed here so it can be used even after 1102 * we've called dismiss(), which attempts to null mSearchable. 1103 */ 1104 private void sendLaunchIntent(final String action, final Uri data, final String query, 1105 final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) { 1106 Intent launcher = new Intent(action); 1107 1108 if (query != null) { 1109 launcher.putExtra(SearchManager.QUERY, query); 1110 } 1111 1112 if (data != null) { 1113 launcher.setData(data); 1114 } 1115 1116 if (appData != null) { 1117 launcher.putExtra(SearchManager.APP_DATA, appData); 1118 } 1119 1120 // add launch info (action key, etc.) 1121 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 1122 launcher.putExtra(SearchManager.ACTION_KEY, actionKey); 1123 launcher.putExtra(SearchManager.ACTION_MSG, actionMsg); 1124 } 1125 1126 // attempt to enforce security requirement (no 3rd-party intents) 1127 launcher.setComponent(si.mSearchActivity); 1128 1129 getContext().startActivity(launcher); 1130 } 1131 1132 /** 1133 * Shared code for launching a query from a suggestion. 1134 * @param ca The cursor adapter containing the suggestions 1135 * @param position The suggestion we'll be launching from 1136 * @return true if a successful launch, false if could not (e.g. bad position) 1137 */ 1138 private boolean launchSuggestion(CursorAdapter ca, int position) { 1139 Cursor c = ca.getCursor(); 1140 if ((c != null) && c.moveToPosition(position)) { 1141 setupSuggestionIntent(c, mSearchable); 1142 1143 final Bundle appData = mAppSearchData; 1144 SearchableInfo si = mSearchable; 1145 String suggestionAction = mSuggestionAction; 1146 Uri suggestionData = mSuggestionData; 1147 String suggestionQuery = mSuggestionQuery; 1148 dismiss(); 1149 sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, appData, 1150 KeyEvent.KEYCODE_UNKNOWN, null, si); 1151 return true; 1152 } 1153 return false; 1154 } 1155 1156 /** 1157 * When a particular suggestion has been selected, perform the various lookups required 1158 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 1159 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 1160 * the suggestion includes a data id. 1161 * 1162 * NOTE: Return values are in member variables mSuggestionAction & mSuggestionData. 1163 * 1164 * @param c The suggestions cursor, moved to the row of the user's selection 1165 * @param si The searchable activity's info record 1166 */ 1167 void setupSuggestionIntent(Cursor c, SearchableInfo si) { 1168 try { 1169 // use specific action if supplied, or default action if supplied, or fixed default 1170 mSuggestionAction = null; 1171 int mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 1172 if (mColumn >= 0) { 1173 final String action = c.getString(mColumn); 1174 if (action != null) { 1175 mSuggestionAction = action; 1176 } 1177 } 1178 if (mSuggestionAction == null) { 1179 mSuggestionAction = si.getSuggestIntentAction(); 1180 } 1181 if (mSuggestionAction == null) { 1182 mSuggestionAction = Intent.ACTION_SEARCH; 1183 } 1184 1185 // use specific data if supplied, or default data if supplied 1186 String data = null; 1187 mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1188 if (mColumn >= 0) { 1189 final String rowData = c.getString(mColumn); 1190 if (rowData != null) { 1191 data = rowData; 1192 } 1193 } 1194 if (data == null) { 1195 data = si.getSuggestIntentData(); 1196 } 1197 1198 // then, if an ID was provided, append it. 1199 if (data != null) { 1200 mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 1201 if (mColumn >= 0) { 1202 final String id = c.getString(mColumn); 1203 if (id != null) { 1204 data = data + "/" + Uri.encode(id); 1205 } 1206 } 1207 } 1208 mSuggestionData = (data == null) ? null : Uri.parse(data); 1209 1210 mSuggestionQuery = null; 1211 mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); 1212 if (mColumn >= 0) { 1213 final String query = c.getString(mColumn); 1214 if (query != null) { 1215 mSuggestionQuery = query; 1216 } 1217 } 1218 } catch (RuntimeException e ) { 1219 int rowNum; 1220 try { // be really paranoid now 1221 rowNum = c.getPosition(); 1222 } catch (RuntimeException e2 ) { 1223 rowNum = -1; 1224 } 1225 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 1226 " returned exception" + e.toString()); 1227 } 1228 } 1229 1230 /** 1231 * For a given suggestion and a given cursor row, get the action message. If not provided 1232 * by the specific row/column, also check for a single definition (for the action key). 1233 * 1234 * @param c The cursor providing suggestions 1235 * @param actionKey The actionkey record being examined 1236 * 1237 * @return Returns a string, or null if no action key message for this suggestion 1238 */ 1239 private String getActionKeyMessage(Cursor c, final SearchableInfo.ActionKeyInfo actionKey) { 1240 String result = null; 1241 // check first in the cursor data, for a suggestion-specific message 1242 final String column = actionKey.mSuggestActionMsgColumn; 1243 if (column != null) { 1244 try { 1245 int colId = c.getColumnIndexOrThrow(column); 1246 result = c.getString(colId); 1247 } catch (RuntimeException e) { 1248 // OK - result is already null 1249 } 1250 } 1251 // If the cursor didn't give us a message, see if there's a single message defined 1252 // for the actionkey (for all suggestions) 1253 if (result == null) { 1254 result = actionKey.mSuggestActionMsg; 1255 } 1256 return result; 1257 } 1258 1259 /** 1260 * Local subclass for AutoCompleteTextView 1261 * 1262 * This exists entirely to override the threshold method. Otherwise we just use the class 1263 * as-is. 1264 */ 1265 public static class SearchAutoComplete extends AutoCompleteTextView { 1266 1267 public SearchAutoComplete(Context context) { 1268 super(null); 1269 } 1270 1271 public SearchAutoComplete(Context context, AttributeSet attrs) { 1272 super(context, attrs); 1273 } 1274 1275 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1276 super(context, attrs, defStyle); 1277 } 1278 1279 /** 1280 * We never allow ACTV to automatically replace the text, since we use "jamSuggestionQuery" 1281 * to do that. There's no point in letting ACTV do this here, because in the search UI, 1282 * as soon as we click a suggestion, we're going to start shutting things down. 1283 */ 1284 @Override 1285 public void replaceText(CharSequence text) { 1286 } 1287 1288 /** 1289 * We always return true, so that the effective threshold is "zero". This allows us 1290 * to provide "null" suggestions such as "just show me some recent entries". 1291 */ 1292 @Override 1293 public boolean enoughToFilter() { 1294 return true; 1295 } 1296 } 1297 1298 /** 1299 * Support for AutoCompleteTextView-based suggestions 1300 */ 1301 /** 1302 * This class provides the filtering-based interface to suggestions providers. 1303 * It is hardwired in a couple of places to support GoogleSearch - for example, it supports 1304 * two-line suggestions, but it does not support icons. 1305 */ 1306 private static class SuggestionsAdapter extends SimpleCursorAdapter { 1307 private final String TAG = "SuggestionsAdapter"; 1308 1309 SearchableInfo mSearchable; 1310 private Resources mProviderResources; 1311 1312 // These private variables are shared by the filter thread and must be protected 1313 private WeakReference<Cursor> mRecentCursor = new WeakReference<Cursor>(null); 1314 private boolean mNonUserQuery = false; 1315 private AutoCompleteTextView mParentView; 1316 1317 public SuggestionsAdapter(Context context, SearchableInfo searchable, 1318 AutoCompleteTextView actv) { 1319 super(context, -1, null, null, null); 1320 mSearchable = searchable; 1321 mParentView = actv; 1322 1323 // set up provider resources (gives us icons, etc.) 1324 Context activityContext = mSearchable.getActivityContext(mContext); 1325 Context providerContext = mSearchable.getProviderContext(mContext, activityContext); 1326 mProviderResources = providerContext.getResources(); 1327 } 1328 1329 /** 1330 * Set this field (temporarily!) to disable suggestions updating. This allows us 1331 * to change the string in the text view without changing the suggestions list. 1332 */ 1333 public void setNonUserQuery(boolean nonUserQuery) { 1334 synchronized (this) { 1335 mNonUserQuery = nonUserQuery; 1336 } 1337 } 1338 1339 public boolean getNonUserQuery() { 1340 synchronized (this) { 1341 return mNonUserQuery; 1342 } 1343 } 1344 1345 /** 1346 * Use the search suggestions provider to obtain a live cursor. This will be called 1347 * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions). 1348 * The results will be processed in the UI thread and changeCursor() will be called. 1349 * 1350 * In order to provide the Search Mgr functionality of seeing your query change as you 1351 * scroll through the list, we have to be able to jam new text into the string without 1352 * retriggering the suggestions. We do that here via the "nonUserQuery" flag. In that 1353 * case we simply return the existing cursor. 1354 * 1355 * TODO: Dianne suggests that this should simply be promoted into an AutoCompleteTextView 1356 * behavior (perhaps optionally). 1357 * 1358 * TODO: The "nonuserquery" logic has a race condition because it happens in another thread. 1359 * This also needs to be fixed. 1360 */ 1361 @Override 1362 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 1363 String query = (constraint == null) ? "" : constraint.toString(); 1364 Cursor c = null; 1365 synchronized (this) { 1366 if (mNonUserQuery) { 1367 c = mRecentCursor.get(); 1368 mNonUserQuery = false; 1369 } 1370 } 1371 if (c == null) { 1372 c = getSuggestions(mSearchable, query); 1373 synchronized (this) { 1374 mRecentCursor = new WeakReference<Cursor>(c); 1375 } 1376 } 1377 return c; 1378 } 1379 1380 /** 1381 * Overriding changeCursor() allows us to change not only the cursor, but by sampling 1382 * the cursor's columns, the actual display characteristics of the list. 1383 */ 1384 @Override 1385 public void changeCursor(Cursor c) { 1386 1387 // first, check for various conditions that disqualify this cursor 1388 if ((c == null) || (c.getCount() == 0)) { 1389 // no cursor, or cursor with no data 1390 changeCursorAndColumns(null, null, null); 1391 if (c != null) { 1392 c.close(); 1393 } 1394 return; 1395 } 1396 1397 // check cursor before trying to create list views from it 1398 int colId = c.getColumnIndex("_id"); 1399 int col1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); 1400 int col2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); 1401 int colIc1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); 1402 int colIc2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); 1403 1404 boolean minimal = (colId >= 0) && (col1 >= 0); 1405 boolean hasIcons = (colIc1 >= 0) && (colIc2 >= 0); 1406 boolean has2Lines = col2 >= 0; 1407 1408 if (minimal) { 1409 int layout; 1410 String[] from; 1411 int[] to; 1412 1413 if (hasIcons) { 1414 if (has2Lines) { 1415 layout = com.android.internal.R.layout.search_dropdown_item_icons_2line; 1416 from = TWO_LINE_ICONS_FROM; 1417 to = TWO_LINE_ICONS_TO; 1418 } else { 1419 layout = com.android.internal.R.layout.search_dropdown_item_icons_1line; 1420 from = ONE_LINE_ICONS_FROM; 1421 to = ONE_LINE_ICONS_TO; 1422 } 1423 } else { 1424 if (has2Lines) { 1425 layout = com.android.internal.R.layout.search_dropdown_item_2line; 1426 from = TWO_LINE_FROM; 1427 to = TWO_LINE_TO; 1428 } else { 1429 layout = com.android.internal.R.layout.search_dropdown_item_1line; 1430 from = ONE_LINE_FROM; 1431 to = ONE_LINE_TO; 1432 } 1433 } 1434 // Force the underlying ListView to discard and reload all layouts 1435 // (Note, this should be optimized for cases where layout/cursor remain same) 1436 mParentView.resetListAndClearViews(); 1437 // Now actually set up the cursor, columns, and the list view 1438 changeCursorAndColumns(c, from, to); 1439 setViewResource(layout); 1440 } else { 1441 // Provide some help for developers instead of just silently discarding 1442 Log.w(LOG_TAG, "Suggestions cursor discarded due to missing required columns."); 1443 changeCursorAndColumns(null, null, null); 1444 c.close(); 1445 } 1446 if ((colIc1 >= 0) != (colIc2 >= 0)) { 1447 Log.w(LOG_TAG, "Suggestion icon column(s) discarded, must be 0 or 2 columns."); 1448 } 1449 } 1450 1451 /** 1452 * Overriding this allows us to write the selected query back into the box. 1453 * NOTE: This is a vastly simplified version of SearchDialog.jamQuery() and does 1454 * not universally support the search API. But it is sufficient for Google Search. 1455 */ 1456 @Override 1457 public CharSequence convertToString(Cursor cursor) { 1458 CharSequence result = null; 1459 if (cursor != null) { 1460 int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); 1461 if (column >= 0) { 1462 final String query = cursor.getString(column); 1463 if (query != null) { 1464 result = query; 1465 } 1466 } 1467 } 1468 return result; 1469 } 1470 1471 /** 1472 * Get the query cursor for the search suggestions. 1473 * 1474 * TODO this is functionally identical to the version in SearchDialog.java. Perhaps it 1475 * could be hoisted into SearchableInfo or some other shared spot. 1476 * 1477 * @param query The search text entered (so far) 1478 * @return Returns a cursor with suggestions, or null if no suggestions 1479 */ 1480 private Cursor getSuggestions(final SearchableInfo searchable, final String query) { 1481 Cursor cursor = null; 1482 if (searchable.getSuggestAuthority() != null) { 1483 try { 1484 StringBuilder uriStr = new StringBuilder("content://"); 1485 uriStr.append(searchable.getSuggestAuthority()); 1486 1487 // if content path provided, insert it now 1488 final String contentPath = searchable.getSuggestPath(); 1489 if (contentPath != null) { 1490 uriStr.append('/'); 1491 uriStr.append(contentPath); 1492 } 1493 1494 // append standard suggestion query path 1495 uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY); 1496 1497 // inject query, either as selection args or inline 1498 String[] selArgs = null; 1499 if (searchable.getSuggestSelection() != null) { // use selection if provided 1500 selArgs = new String[] {query}; 1501 } else { 1502 uriStr.append('/'); // no sel, use REST pattern 1503 uriStr.append(Uri.encode(query)); 1504 } 1505 1506 // finally, make the query 1507 cursor = mContext.getContentResolver().query( 1508 Uri.parse(uriStr.toString()), null, 1509 searchable.getSuggestSelection(), selArgs, 1510 null); 1511 } catch (RuntimeException e) { 1512 Log.w(TAG, "Search Suggestions query returned exception " + e.toString()); 1513 cursor = null; 1514 } 1515 } 1516 1517 return cursor; 1518 } 1519 1520 /** 1521 * Overriding this allows us to affect the way that an icon is loaded. Specifically, 1522 * we can be more controlling about the resource path (and allow icons to come from other 1523 * packages). 1524 * 1525 * TODO: This is 100% identical to the version in SearchDialog.java 1526 * 1527 * @param v ImageView to receive an image 1528 * @param value the value retrieved from the cursor 1529 */ 1530 @Override 1531 public void setViewImage(ImageView v, String value) { 1532 int resID; 1533 Drawable img = null; 1534 1535 try { 1536 resID = Integer.parseInt(value); 1537 if (resID != 0) { 1538 img = mProviderResources.getDrawable(resID); 1539 } 1540 } catch (NumberFormatException nfe) { 1541 // img = null; 1542 } catch (NotFoundException e2) { 1543 // img = null; 1544 } 1545 1546 // finally, set the image to whatever we've gotten 1547 v.setImageDrawable(img); 1548 } 1549 1550 /** 1551 * This method is overridden purely to provide a bit of protection against 1552 * flaky content providers. 1553 * 1554 * TODO: This is 100% identical to the version in SearchDialog.java 1555 * 1556 * @see android.widget.ListAdapter#getView(int, View, ViewGroup) 1557 */ 1558 @Override 1559 public View getView(int position, View convertView, ViewGroup parent) { 1560 try { 1561 return super.getView(position, convertView, parent); 1562 } catch (RuntimeException e) { 1563 Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString()); 1564 // what can I return here? 1565 View v = newView(mContext, mCursor, parent); 1566 if (v != null) { 1567 TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1); 1568 tv.setText(e.toString()); 1569 } 1570 return v; 1571 } 1572 } 1573 1574 } 1575 1576 /** 1577 * Implements OnItemClickListener 1578 */ 1579 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1580 // Log.d(LOG_TAG, "onItemClick() position " + position); 1581 launchSuggestion(mSuggestionsAdapter, position); 1582 } 1583 1584 /** 1585 * Implements OnItemSelectedListener 1586 */ 1587 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1588 // Log.d(LOG_TAG, "onItemSelected() position " + position); 1589 jamSuggestionQuery(true, parent, position); 1590 } 1591 1592 /** 1593 * Implements OnItemSelectedListener 1594 */ 1595 public void onNothingSelected(AdapterView<?> parent) { 1596 // Log.d(LOG_TAG, "onNothingSelected()"); 1597 } 1598 1599 /** 1600 * Debugging Support 1601 */ 1602 1603 /** 1604 * For debugging only, sample the millisecond clock and log it. 1605 * Uses AtomicLong so we can use in multiple threads 1606 */ 1607 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis()); 1608 private void dbgLogTiming(final String caller) { 1609 long millis = SystemClock.uptimeMillis(); 1610 long oldTime = mLastLogTime.getAndSet(millis); 1611 long delta = millis - oldTime; 1612 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller; 1613 Log.d(LOG_TAG,report); 1614 } 1615} 1616