SearchDialog.java revision 33b167be343aa6b635113159f9901e9cc48d085e
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 19 20import static android.app.SuggestionsAdapter.getColumnString; 21 22import android.content.ActivityNotFoundException; 23import android.content.ComponentName; 24import android.content.ContentResolver; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.Intent; 28import android.content.pm.ActivityInfo; 29import android.content.pm.PackageManager; 30import android.content.pm.ResolveInfo; 31import android.content.pm.PackageManager.NameNotFoundException; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.graphics.drawable.Drawable; 35import android.net.Uri; 36import android.os.Bundle; 37import android.os.IBinder; 38import android.os.RemoteException; 39import android.os.SystemClock; 40import android.provider.Browser; 41import android.speech.RecognizerIntent; 42import android.text.Editable; 43import android.text.InputType; 44import android.text.TextUtils; 45import android.text.TextWatcher; 46import android.util.AndroidRuntimeException; 47import android.util.AttributeSet; 48import android.util.Log; 49import android.util.Patterns; 50import android.view.ContextThemeWrapper; 51import android.view.Gravity; 52import android.view.KeyEvent; 53import android.view.MotionEvent; 54import android.view.View; 55import android.view.ViewConfiguration; 56import android.view.ViewGroup; 57import android.view.Window; 58import android.view.WindowManager; 59import android.view.inputmethod.EditorInfo; 60import android.view.inputmethod.InputMethodManager; 61import android.widget.AdapterView; 62import android.widget.AutoCompleteTextView; 63import android.widget.Button; 64import android.widget.ImageButton; 65import android.widget.ImageView; 66import android.widget.LinearLayout; 67import android.widget.ListView; 68import android.widget.TextView; 69import android.widget.AdapterView.OnItemClickListener; 70import android.widget.AdapterView.OnItemSelectedListener; 71 72import java.util.ArrayList; 73import java.util.WeakHashMap; 74import java.util.concurrent.atomic.AtomicLong; 75 76/** 77 * Search dialog. This is controlled by the 78 * SearchManager and runs in the current foreground process. 79 * 80 * @hide 81 */ 82public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener { 83 84 // Debugging support 85 private static final boolean DBG = false; 86 private static final String LOG_TAG = "SearchDialog"; 87 private static final boolean DBG_LOG_TIMING = false; 88 89 private static final String INSTANCE_KEY_COMPONENT = "comp"; 90 private static final String INSTANCE_KEY_APPDATA = "data"; 91 private static final String INSTANCE_KEY_STORED_APPDATA = "sData"; 92 private static final String INSTANCE_KEY_USER_QUERY = "uQry"; 93 94 // The string used for privateImeOptions to identify to the IME that it should not show 95 // a microphone button since one already exists in the search dialog. 96 private static final String IME_OPTION_NO_MICROPHONE = "nm"; 97 98 private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12; 99 private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; 100 101 // views & widgets 102 private TextView mBadgeLabel; 103 private ImageView mAppIcon; 104 private SearchAutoComplete mSearchAutoComplete; 105 private Button mGoButton; 106 private ImageButton mVoiceButton; 107 private View mSearchPlate; 108 private Drawable mWorkingSpinner; 109 110 // interaction with searchable application 111 private SearchableInfo mSearchable; 112 private ComponentName mLaunchComponent; 113 private Bundle mAppSearchData; 114 private Context mActivityContext; 115 private SearchManager mSearchManager; 116 117 // For voice searching 118 private final Intent mVoiceWebSearchIntent; 119 private final Intent mVoiceAppSearchIntent; 120 121 // support for AutoCompleteTextView suggestions display 122 private SuggestionsAdapter mSuggestionsAdapter; 123 124 // Whether to rewrite queries when selecting suggestions 125 private static final boolean REWRITE_QUERIES = true; 126 127 // The query entered by the user. This is not changed when selecting a suggestion 128 // that modifies the contents of the text field. But if the user then edits 129 // the suggestion, the resulting string is saved. 130 private String mUserQuery; 131 132 // A weak map of drawables we've gotten from other packages, so we don't load them 133 // more than once. 134 private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache = 135 new WeakHashMap<String, Drawable.ConstantState>(); 136 137 // Last known IME options value for the search edit text. 138 private int mSearchAutoCompleteImeOptions; 139 140 /** 141 * Constructor - fires it up and makes it look like the search UI. 142 * 143 * @param context Application Context we can use for system acess 144 */ 145 public SearchDialog(Context context, SearchManager searchManager) { 146 super(context, com.android.internal.R.style.Theme_SearchBar); 147 148 // Save voice intent for later queries/launching 149 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); 150 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 151 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 152 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); 153 154 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 155 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 156 mSearchManager = searchManager; 157 } 158 159 /** 160 * Create the search dialog and any resources that are used for the 161 * entire lifetime of the dialog. 162 */ 163 @Override 164 protected void onCreate(Bundle savedInstanceState) { 165 super.onCreate(savedInstanceState); 166 167 Window theWindow = getWindow(); 168 WindowManager.LayoutParams lp = theWindow.getAttributes(); 169 lp.width = ViewGroup.LayoutParams.MATCH_PARENT; 170 // taking up the whole window (even when transparent) is less than ideal, 171 // but necessary to show the popup window until the window manager supports 172 // having windows anchored by their parent but not clipped by them. 173 lp.height = ViewGroup.LayoutParams.MATCH_PARENT; 174 lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL; 175 lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 176 theWindow.setAttributes(lp); 177 178 // Touching outside of the search dialog will dismiss it 179 setCanceledOnTouchOutside(true); 180 } 181 182 /** 183 * We recreate the dialog view each time it becomes visible so as to limit 184 * the scope of any problems with the contained resources. 185 */ 186 private void createContentView() { 187 setContentView(com.android.internal.R.layout.search_bar); 188 189 // get the view elements for local access 190 SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar); 191 searchBar.setSearchDialog(this); 192 193 mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge); 194 mSearchAutoComplete = (SearchAutoComplete) 195 findViewById(com.android.internal.R.id.search_src_text); 196 mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon); 197 mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn); 198 mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn); 199 mSearchPlate = findViewById(com.android.internal.R.id.search_plate); 200 mWorkingSpinner = getContext().getResources(). 201 getDrawable(com.android.internal.R.drawable.search_spinner); 202 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( 203 null, null, mWorkingSpinner, null); 204 setWorking(false); 205 206 // attach listeners 207 mSearchAutoComplete.addTextChangedListener(mTextWatcher); 208 mSearchAutoComplete.setOnKeyListener(mTextKeyListener); 209 mSearchAutoComplete.setOnItemClickListener(this); 210 mSearchAutoComplete.setOnItemSelectedListener(this); 211 mGoButton.setOnClickListener(mGoButtonClickListener); 212 mGoButton.setOnKeyListener(mButtonsKeyListener); 213 mVoiceButton.setOnClickListener(mVoiceButtonClickListener); 214 mVoiceButton.setOnKeyListener(mButtonsKeyListener); 215 216 // pre-hide all the extraneous elements 217 mBadgeLabel.setVisibility(View.GONE); 218 219 // Additional adjustments to make Dialog work for Search 220 mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions(); 221 } 222 223 /** 224 * Set up the search dialog 225 * 226 * @return true if search dialog launched, false if not 227 */ 228 public boolean show(String initialQuery, boolean selectInitialQuery, 229 ComponentName componentName, Bundle appSearchData) { 230 boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData); 231 if (success) { 232 // Display the drop down as soon as possible instead of waiting for the rest of the 233 // pending UI stuff to get done, so that things appear faster to the user. 234 mSearchAutoComplete.showDropDownAfterLayout(); 235 } 236 return success; 237 } 238 239 /** 240 * Does the rest of the work required to show the search dialog. Called by 241 * {@link #show(String, boolean, ComponentName, Bundle)} and 242 * 243 * @return true if search dialog showed, false if not 244 */ 245 private boolean doShow(String initialQuery, boolean selectInitialQuery, 246 ComponentName componentName, Bundle appSearchData) { 247 // set up the searchable and show the dialog 248 if (!show(componentName, appSearchData)) { 249 return false; 250 } 251 252 // finally, load the user's initial text (which may trigger suggestions) 253 setUserQuery(initialQuery); 254 if (selectInitialQuery) { 255 mSearchAutoComplete.selectAll(); 256 } 257 258 return true; 259 } 260 261 /** 262 * Sets up the search dialog and shows it. 263 * 264 * @return <code>true</code> if search dialog launched 265 */ 266 private boolean show(ComponentName componentName, Bundle appSearchData) { 267 268 if (DBG) { 269 Log.d(LOG_TAG, "show(" + componentName + ", " 270 + appSearchData + ")"); 271 } 272 273 SearchManager searchManager = (SearchManager) 274 mContext.getSystemService(Context.SEARCH_SERVICE); 275 // Try to get the searchable info for the provided component. 276 mSearchable = searchManager.getSearchableInfo(componentName); 277 278 if (mSearchable == null) { 279 return false; 280 } 281 282 mLaunchComponent = componentName; 283 mAppSearchData = appSearchData; 284 mActivityContext = mSearchable.getActivityContext(getContext()); 285 286 // show the dialog. this will call onStart(). 287 if (!isShowing()) { 288 // Recreate the search bar view every time the dialog is shown, to get rid 289 // of any bad state in the AutoCompleteTextView etc 290 createContentView(); 291 292 show(); 293 } 294 updateUI(); 295 296 return true; 297 } 298 299 /** 300 * The search dialog is being dismissed, so handle all of the local shutdown operations. 301 * 302 * This function is designed to be idempotent so that dismiss() can be safely called at any time 303 * (even if already closed) and more likely to really dump any memory. No leaks! 304 */ 305 @Override 306 public void onStop() { 307 super.onStop(); 308 309 closeSuggestionsAdapter(); 310 311 // dump extra memory we're hanging on to 312 mLaunchComponent = null; 313 mAppSearchData = null; 314 mSearchable = null; 315 mUserQuery = null; 316 } 317 318 /** 319 * Sets the search dialog to the 'working' state, which shows a working spinner in the 320 * right hand size of the text field. 321 * 322 * @param working true to show spinner, false to hide spinner 323 */ 324 public void setWorking(boolean working) { 325 mWorkingSpinner.setAlpha(working ? 255 : 0); 326 mWorkingSpinner.setVisible(working, false); 327 mWorkingSpinner.invalidateSelf(); 328 } 329 330 /** 331 * Closes and gets rid of the suggestions adapter. 332 */ 333 private void closeSuggestionsAdapter() { 334 // remove the adapter from the autocomplete first, to avoid any updates 335 // when we drop the cursor 336 mSearchAutoComplete.setAdapter((SuggestionsAdapter)null); 337 // close any leftover cursor 338 if (mSuggestionsAdapter != null) { 339 mSuggestionsAdapter.close(); 340 } 341 mSuggestionsAdapter = null; 342 } 343 344 /** 345 * Save the minimal set of data necessary to recreate the search 346 * 347 * @return A bundle with the state of the dialog, or {@code null} if the search 348 * dialog is not showing. 349 */ 350 @Override 351 public Bundle onSaveInstanceState() { 352 if (!isShowing()) return null; 353 354 Bundle bundle = new Bundle(); 355 356 // setup info so I can recreate this particular search 357 bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent); 358 bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData); 359 bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 360 361 return bundle; 362 } 363 364 /** 365 * Restore the state of the dialog from a previously saved bundle. 366 * 367 * TODO: go through this and make sure that it saves everything that is saved 368 * 369 * @param savedInstanceState The state of the dialog previously saved by 370 * {@link #onSaveInstanceState()}. 371 */ 372 @Override 373 public void onRestoreInstanceState(Bundle savedInstanceState) { 374 if (savedInstanceState == null) return; 375 376 ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT); 377 Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA); 378 String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 379 380 // show the dialog. 381 if (!doShow(userQuery, false, launchComponent, appSearchData)) { 382 // for some reason, we couldn't re-instantiate 383 return; 384 } 385 } 386 387 /** 388 * Called after resources have changed, e.g. after screen rotation or locale change. 389 */ 390 public void onConfigurationChanged() { 391 if (mSearchable != null && isShowing()) { 392 // Redraw (resources may have changed) 393 updateSearchButton(); 394 updateSearchAppIcon(); 395 updateSearchBadge(); 396 updateQueryHint(); 397 mSearchAutoComplete.showDropDownAfterLayout(); 398 } 399 } 400 401 /** 402 * Update the UI according to the info in the current value of {@link #mSearchable}. 403 */ 404 private void updateUI() { 405 if (mSearchable != null) { 406 mDecor.setVisibility(View.VISIBLE); 407 updateSearchAutoComplete(); 408 updateSearchButton(); 409 updateSearchAppIcon(); 410 updateSearchBadge(); 411 updateQueryHint(); 412 updateVoiceButton(TextUtils.isEmpty(mUserQuery)); 413 414 // In order to properly configure the input method (if one is being used), we 415 // need to let it know if we'll be providing suggestions. Although it would be 416 // difficult/expensive to know if every last detail has been configured properly, we 417 // can at least see if a suggestions provider has been configured, and use that 418 // as our trigger. 419 int inputType = mSearchable.getInputType(); 420 // We only touch this if the input type is set up for text (which it almost certainly 421 // should be, in the case of search!) 422 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { 423 // The existence of a suggestions authority is the proxy for "suggestions 424 // are available here" 425 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 426 if (mSearchable.getSuggestAuthority() != null) { 427 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; 428 } 429 } 430 mSearchAutoComplete.setInputType(inputType); 431 mSearchAutoCompleteImeOptions = mSearchable.getImeOptions(); 432 mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions); 433 434 // If the search dialog is going to show a voice search button, then don't let 435 // the soft keyboard display a microphone button if it would have otherwise. 436 if (mSearchable.getVoiceSearchEnabled()) { 437 mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE); 438 } else { 439 mSearchAutoComplete.setPrivateImeOptions(null); 440 } 441 } 442 } 443 444 /** 445 * Updates the auto-complete text view. 446 */ 447 private void updateSearchAutoComplete() { 448 // close any existing suggestions adapter 449 closeSuggestionsAdapter(); 450 451 mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation 452 mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold()); 453 // we dismiss the entire dialog instead 454 mSearchAutoComplete.setDropDownDismissedOnCompletion(false); 455 456 mSearchAutoComplete.setForceIgnoreOutsideTouch(true); 457 458 // attach the suggestions adapter, if suggestions are available 459 // The existence of a suggestions authority is the proxy for "suggestions available here" 460 if (mSearchable.getSuggestAuthority() != null) { 461 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable, 462 mOutsideDrawablesCache); 463 mSearchAutoComplete.setAdapter(mSuggestionsAdapter); 464 } 465 } 466 467 private void updateSearchButton() { 468 String textLabel = null; 469 Drawable iconLabel = null; 470 int textId = mSearchable.getSearchButtonText(); 471 if (isBrowserSearch()){ 472 iconLabel = getContext().getResources() 473 .getDrawable(com.android.internal.R.drawable.ic_btn_search_play); 474 } else if (textId != 0) { 475 textLabel = mActivityContext.getResources().getString(textId); 476 } else { 477 iconLabel = getContext().getResources(). 478 getDrawable(com.android.internal.R.drawable.ic_btn_search); 479 } 480 mGoButton.setText(textLabel); 481 mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null); 482 } 483 484 private void updateSearchAppIcon() { 485 if (isBrowserSearch()) { 486 mAppIcon.setImageResource(0); 487 mAppIcon.setVisibility(View.GONE); 488 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL, 489 mSearchPlate.getPaddingTop(), 490 mSearchPlate.getPaddingRight(), 491 mSearchPlate.getPaddingBottom()); 492 } else { 493 PackageManager pm = getContext().getPackageManager(); 494 Drawable icon; 495 try { 496 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0); 497 icon = pm.getApplicationIcon(info.applicationInfo); 498 if (DBG) Log.d(LOG_TAG, "Using app-specific icon"); 499 } catch (NameNotFoundException e) { 500 icon = pm.getDefaultActivityIcon(); 501 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon"); 502 } 503 mAppIcon.setImageDrawable(icon); 504 mAppIcon.setVisibility(View.VISIBLE); 505 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, 506 mSearchPlate.getPaddingTop(), 507 mSearchPlate.getPaddingRight(), 508 mSearchPlate.getPaddingBottom()); 509 } 510 } 511 512 /** 513 * Setup the search "Badge" if requested by mode flags. 514 */ 515 private void updateSearchBadge() { 516 // assume both hidden 517 int visibility = View.GONE; 518 Drawable icon = null; 519 CharSequence text = null; 520 521 // optionally show one or the other. 522 if (mSearchable.useBadgeIcon()) { 523 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId()); 524 visibility = View.VISIBLE; 525 if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId()); 526 } else if (mSearchable.useBadgeLabel()) { 527 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString(); 528 visibility = View.VISIBLE; 529 if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId()); 530 } 531 532 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); 533 mBadgeLabel.setText(text); 534 mBadgeLabel.setVisibility(visibility); 535 } 536 537 /** 538 * Update the hint in the query text field. 539 */ 540 private void updateQueryHint() { 541 if (isShowing()) { 542 String hint = null; 543 if (mSearchable != null) { 544 int hintId = mSearchable.getHintId(); 545 if (hintId != 0) { 546 hint = mActivityContext.getString(hintId); 547 } 548 } 549 mSearchAutoComplete.setHint(hint); 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.getVoiceSearchEnabled() && empty) { 563 Intent testIntent = null; 564 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 565 testIntent = mVoiceWebSearchIntent; 566 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 567 testIntent = mVoiceAppSearchIntent; 568 } 569 if (testIntent != null) { 570 ResolveInfo ri = getContext().getPackageManager(). 571 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); 572 if (ri != null) { 573 visibility = View.VISIBLE; 574 } 575 } 576 } 577 mVoiceButton.setVisibility(visibility); 578 } 579 580 /** Called by SuggestionsAdapter when the cursor contents changed. */ 581 void onDataSetChanged() { 582 if (mSearchAutoComplete != null && mSuggestionsAdapter != null) { 583 mSearchAutoComplete.onFilterComplete(mSuggestionsAdapter.getCount()); 584 } 585 } 586 587 /** 588 * Hack to determine whether this is the browser, so we can adjust the UI. 589 */ 590 private boolean isBrowserSearch() { 591 return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/"); 592 } 593 594 /* 595 * Listeners of various types 596 */ 597 598 /** 599 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the 600 * touch is outside the window. But the window includes space for the drop-down, 601 * so we also cancel on taps outside the search bar when the drop-down is not showing. 602 */ 603 @Override 604 public boolean onTouchEvent(MotionEvent event) { 605 // cancel if the drop-down is not showing and the touch event was outside the search plate 606 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) { 607 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate."); 608 cancel(); 609 return true; 610 } 611 // Let Dialog handle events outside the window while the pop-up is showing. 612 return super.onTouchEvent(event); 613 } 614 615 private boolean isOutOfBounds(View v, MotionEvent event) { 616 final int x = (int) event.getX(); 617 final int y = (int) event.getY(); 618 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); 619 return (x < -slop) || (y < -slop) 620 || (x > (v.getWidth()+slop)) 621 || (y > (v.getHeight()+slop)); 622 } 623 624 /** 625 * Dialog's OnKeyListener implements various search-specific functionality 626 * 627 * @param keyCode This is the keycode of the typed key, and is the same value as 628 * found in the KeyEvent parameter. 629 * @param event The complete event record for the typed key 630 * 631 * @return Return true if the event was handled here, or false if not. 632 */ 633 @Override 634 public boolean onKeyDown(int keyCode, KeyEvent event) { 635 if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")"); 636 if (mSearchable == null) { 637 return false; 638 } 639 640 // if it's an action specified by the searchable activity, launch the 641 // entered query with the action key 642 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 643 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 644 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 645 return true; 646 } 647 648 return super.onKeyDown(keyCode, event); 649 } 650 651 /** 652 * Callback to watch the textedit field for empty/non-empty 653 */ 654 private TextWatcher mTextWatcher = new TextWatcher() { 655 656 public void beforeTextChanged(CharSequence s, int start, int before, int after) { } 657 658 public void onTextChanged(CharSequence s, int start, 659 int before, int after) { 660 if (DBG_LOG_TIMING) { 661 dbgLogTiming("onTextChanged()"); 662 } 663 if (mSearchable == null) { 664 return; 665 } 666 updateWidgetState(); 667 if (!mSearchAutoComplete.isPerformingCompletion()) { 668 // The user changed the query, remember it. 669 mUserQuery = s == null ? "" : s.toString(); 670 } 671 updateVoiceButton(mSearchAutoComplete.isEmpty()); 672 } 673 674 public void afterTextChanged(Editable s) { 675 if (mSearchable == null) { 676 return; 677 } 678 if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) { 679 // The user changed the query, check if it is a URL and if so change the search 680 // button in the soft keyboard to the 'Go' button. 681 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)) 682 | EditorInfo.IME_ACTION_GO; 683 if (options != mSearchAutoCompleteImeOptions) { 684 mSearchAutoCompleteImeOptions = options; 685 mSearchAutoComplete.setImeOptions(options); 686 // This call is required to update the soft keyboard UI with latest IME flags. 687 mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType()); 688 } 689 } 690 } 691 }; 692 693 /** 694 * Enable/Disable the go button based on edit text state (any text?) 695 */ 696 private void updateWidgetState() { 697 // enable the button if we have one or more non-space characters 698 boolean enabled = !mSearchAutoComplete.isEmpty(); 699 if (isBrowserSearch()) { 700 // In the browser, we hide the search button when there is no text 701 if (enabled) { 702 mSearchAutoComplete.setBackgroundResource( 703 com.android.internal.R.drawable.textfield_search); 704 mGoButton.setVisibility(View.VISIBLE); 705 // Just to be sure 706 mGoButton.setEnabled(true); 707 mGoButton.setFocusable(true); 708 } else { 709 mSearchAutoComplete.setBackgroundResource( 710 com.android.internal.R.drawable.textfield_search_empty); 711 mGoButton.setVisibility(View.GONE); 712 } 713 } else { 714 // Elsewhere we just disable the button 715 mGoButton.setEnabled(enabled); 716 mGoButton.setFocusable(enabled); 717 } 718 } 719 720 /** 721 * React to typing in the GO search button by refocusing to EditText. 722 * Continue typing the query. 723 */ 724 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() { 725 public boolean onKey(View v, int keyCode, KeyEvent event) { 726 // guard against possible race conditions 727 if (mSearchable == null) { 728 return false; 729 } 730 731 if (!event.isSystem() && 732 (keyCode != KeyEvent.KEYCODE_DPAD_UP) && 733 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && 734 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && 735 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { 736 // restore focus and give key to EditText ... 737 if (mSearchAutoComplete.requestFocus()) { 738 return mSearchAutoComplete.dispatchKeyEvent(event); 739 } 740 } 741 742 return false; 743 } 744 }; 745 746 /** 747 * React to a click in the GO button by launching a search. 748 */ 749 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() { 750 public void onClick(View v) { 751 // guard against possible race conditions 752 if (mSearchable == null) { 753 return; 754 } 755 launchQuerySearch(); 756 } 757 }; 758 759 /** 760 * React to a click in the voice search button. 761 */ 762 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() { 763 public void onClick(View v) { 764 // guard against possible race conditions 765 if (mSearchable == null) { 766 return; 767 } 768 SearchableInfo searchable = mSearchable; 769 try { 770 if (searchable.getVoiceSearchLaunchWebSearch()) { 771 getContext().startActivity(mVoiceWebSearchIntent); 772 } else if (searchable.getVoiceSearchLaunchRecognizer()) { 773 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, 774 searchable); 775 getContext().startActivity(appSearchIntent); 776 } 777 } catch (ActivityNotFoundException e) { 778 // Should not happen, since we check the availability of 779 // voice search before showing the button. But just in case... 780 Log.w(LOG_TAG, "Could not find voice search activity"); 781 } 782 dismiss(); 783 } 784 }; 785 786 /** 787 * Create and return an Intent that can launch the voice search activity, perform a specific 788 * voice transcription, and forward the results to the searchable activity. 789 * 790 * @param baseIntent The voice app search intent to start from 791 * @return A completely-configured intent ready to send to the voice search activity 792 */ 793 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { 794 ComponentName searchActivity = searchable.getSearchActivity(); 795 796 // create the necessary intent to set up a search-and-forward operation 797 // in the voice search system. We have to keep the bundle separate, 798 // because it becomes immutable once it enters the PendingIntent 799 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 800 queryIntent.setComponent(searchActivity); 801 PendingIntent pending = PendingIntent.getActivity( 802 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT); 803 804 // Now set up the bundle that will be inserted into the pending intent 805 // when it's time to do the search. We always build it here (even if empty) 806 // because the voice search activity will always need to insert "QUERY" into 807 // it anyway. 808 Bundle queryExtras = new Bundle(); 809 if (mAppSearchData != null) { 810 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData); 811 } 812 813 // Now build the intent to launch the voice search. Add all necessary 814 // extras to launch the voice recognizer, and then all the necessary extras 815 // to forward the results to the searchable activity 816 Intent voiceIntent = new Intent(baseIntent); 817 818 // Add all of the configuration options supplied by the searchable's metadata 819 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 820 String prompt = null; 821 String language = null; 822 int maxResults = 1; 823 Resources resources = mActivityContext.getResources(); 824 if (searchable.getVoiceLanguageModeId() != 0) { 825 languageModel = resources.getString(searchable.getVoiceLanguageModeId()); 826 } 827 if (searchable.getVoicePromptTextId() != 0) { 828 prompt = resources.getString(searchable.getVoicePromptTextId()); 829 } 830 if (searchable.getVoiceLanguageId() != 0) { 831 language = resources.getString(searchable.getVoiceLanguageId()); 832 } 833 if (searchable.getVoiceMaxResults() != 0) { 834 maxResults = searchable.getVoiceMaxResults(); 835 } 836 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 837 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 838 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 839 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 840 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, 841 searchActivity == null ? null : searchActivity.toShortString()); 842 843 // Add the values that configure forwarding the results 844 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 845 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 846 847 return voiceIntent; 848 } 849 850 /** 851 * Corrects http/https typo errors in the given url string, and if the protocol specifier was 852 * not present defaults to http. 853 * 854 * @param inUrl URL to check and fix 855 * @return fixed URL string. 856 */ 857 private String fixUrl(String inUrl) { 858 if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) 859 return inUrl; 860 861 if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) { 862 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { 863 inUrl = inUrl.replaceFirst("/", "//"); 864 } else { 865 inUrl = inUrl.replaceFirst(":", "://"); 866 } 867 } 868 869 if (inUrl.indexOf("://") == -1) { 870 inUrl = "http://" + inUrl; 871 } 872 873 return inUrl; 874 } 875 876 /** 877 * React to the user typing "enter" or other hardwired keys while typing in the search box. 878 * This handles these special keys while the edit box has focus. 879 */ 880 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { 881 public boolean onKey(View v, int keyCode, KeyEvent event) { 882 // guard against possible race conditions 883 if (mSearchable == null) { 884 return false; 885 } 886 887 if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()"); 888 if (DBG) { 889 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event 890 + "), selection: " + mSearchAutoComplete.getListSelection()); 891 } 892 893 // If a suggestion is selected, handle enter, search key, and action keys 894 // as presses on the selected suggestion 895 if (mSearchAutoComplete.isPopupShowing() && 896 mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) { 897 return onSuggestionsKey(v, keyCode, event); 898 } 899 900 // If there is text in the query box, handle enter, and action keys 901 // The search key is handled by the dialog's onKeyDown(). 902 if (!mSearchAutoComplete.isEmpty()) { 903 if (keyCode == KeyEvent.KEYCODE_ENTER 904 && event.getAction() == KeyEvent.ACTION_UP) { 905 v.cancelLongPress(); 906 907 // If this is a url entered by the user & we displayed the 'Go' button which 908 // the user clicked, launch the url instead of using it as a search query. 909 if (mSearchable.autoUrlDetect() && 910 (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION) 911 == EditorInfo.IME_ACTION_GO) { 912 Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString())); 913 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 914 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 915 launchIntent(intent); 916 } else { 917 // Launch as a regular search. 918 launchQuerySearch(); 919 } 920 return true; 921 } 922 if (event.getAction() == KeyEvent.ACTION_DOWN) { 923 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 924 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 925 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 926 return true; 927 } 928 } 929 } 930 return false; 931 } 932 }; 933 934 @Override 935 public void hide() { 936 if (!isShowing()) return; 937 938 // We made sure the IME was displayed, so also make sure it is closed 939 // when we go away. 940 InputMethodManager imm = (InputMethodManager)getContext() 941 .getSystemService(Context.INPUT_METHOD_SERVICE); 942 if (imm != null) { 943 imm.hideSoftInputFromWindow( 944 getWindow().getDecorView().getWindowToken(), 0); 945 } 946 947 super.hide(); 948 } 949 950 /** 951 * React to the user typing while in the suggestions list. First, check for action 952 * keys. If not handled, try refocusing regular characters into the EditText. 953 */ 954 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { 955 // guard against possible race conditions (late arrival after dismiss) 956 if (mSearchable == null) { 957 return false; 958 } 959 if (mSuggestionsAdapter == null) { 960 return false; 961 } 962 if (event.getAction() == KeyEvent.ACTION_DOWN) { 963 if (DBG_LOG_TIMING) { 964 dbgLogTiming("onSuggestionsKey()"); 965 } 966 967 // First, check for enter or search (both of which we'll treat as a "click") 968 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 969 int position = mSearchAutoComplete.getListSelection(); 970 return launchSuggestion(position); 971 } 972 973 // Next, check for left/right moves, which we use to "return" the user to the edit view 974 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 975 // give "focus" to text editor, with cursor at the beginning if 976 // left key, at end if right key 977 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic 978 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 979 0 : mSearchAutoComplete.length(); 980 mSearchAutoComplete.setSelection(selPoint); 981 mSearchAutoComplete.setListSelection(0); 982 mSearchAutoComplete.clearListSelection(); 983 mSearchAutoComplete.ensureImeVisible(); 984 985 return true; 986 } 987 988 // Next, check for an "up and out" move 989 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 990 && 0 == mSearchAutoComplete.getListSelection()) { 991 restoreUserQuery(); 992 // let ACTV complete the move 993 return false; 994 } 995 996 // Next, check for an "action key" 997 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 998 if ((actionKey != null) && 999 ((actionKey.getSuggestActionMsg() != null) || 1000 (actionKey.getSuggestActionMsgColumn() != null))) { 1001 // launch suggestion using action key column 1002 int position = mSearchAutoComplete.getListSelection(); 1003 if (position != ListView.INVALID_POSITION) { 1004 Cursor c = mSuggestionsAdapter.getCursor(); 1005 if (c.moveToPosition(position)) { 1006 final String actionMsg = getActionKeyMessage(c, actionKey); 1007 if (actionMsg != null && (actionMsg.length() > 0)) { 1008 return launchSuggestion(position, keyCode, actionMsg); 1009 } 1010 } 1011 } 1012 } 1013 } 1014 return false; 1015 } 1016 1017 /** 1018 * Launch a search for the text in the query text field. 1019 */ 1020 public void launchQuerySearch() { 1021 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 1022 } 1023 1024 /** 1025 * Launch a search for the text in the query text field. 1026 * 1027 * @param actionKey The key code of the action key that was pressed, 1028 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1029 * @param actionMsg The message for the action key that was pressed, 1030 * or <code>null</code> if none. 1031 */ 1032 protected void launchQuerySearch(int actionKey, String actionMsg) { 1033 String query = mSearchAutoComplete.getText().toString(); 1034 String action = Intent.ACTION_SEARCH; 1035 Intent intent = createIntent(action, null, null, query, null, 1036 actionKey, actionMsg); 1037 launchIntent(intent); 1038 } 1039 1040 /** 1041 * Launches an intent based on a suggestion. 1042 * 1043 * @param position The index of the suggestion to create the intent from. 1044 * @return true if a successful launch, false if could not (e.g. bad position). 1045 */ 1046 protected boolean launchSuggestion(int position) { 1047 return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); 1048 } 1049 1050 /** 1051 * Launches an intent based on a suggestion. 1052 * 1053 * @param position The index of the suggestion to create the intent from. 1054 * @param actionKey The key code of the action key that was pressed, 1055 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1056 * @param actionMsg The message for the action key that was pressed, 1057 * or <code>null</code> if none. 1058 * @return true if a successful launch, false if could not (e.g. bad position). 1059 */ 1060 protected boolean launchSuggestion(int position, int actionKey, String actionMsg) { 1061 Cursor c = mSuggestionsAdapter.getCursor(); 1062 if ((c != null) && c.moveToPosition(position)) { 1063 1064 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); 1065 1066 // launch the intent 1067 launchIntent(intent); 1068 1069 return true; 1070 } 1071 return false; 1072 } 1073 1074 /** 1075 * Launches an intent, including any special intent handling. 1076 */ 1077 private void launchIntent(Intent intent) { 1078 if (intent == null) { 1079 return; 1080 } 1081 Log.d(LOG_TAG, "launching " + intent); 1082 try { 1083 // If the intent was created from a suggestion, it will always have an explicit 1084 // component here. 1085 Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI()); 1086 getContext().startActivity(intent); 1087 // If the search switches to a different activity, 1088 // SearchDialogWrapper#performActivityResuming 1089 // will handle hiding the dialog when the next activity starts, but for 1090 // real in-app search, we still need to dismiss the dialog. 1091 dismiss(); 1092 } catch (RuntimeException ex) { 1093 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 1094 } 1095 } 1096 1097 /** 1098 * If the intent is to open an HTTP or HTTPS URL, we set 1099 * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that 1100 * has been opened by us for the same URL will be reused. 1101 */ 1102 private void setBrowserApplicationId(Intent intent) { 1103 Uri data = intent.getData(); 1104 if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) { 1105 String scheme = data.getScheme(); 1106 if (scheme != null && scheme.startsWith("http")) { 1107 intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString()); 1108 } 1109 } 1110 } 1111 1112 /** 1113 * Sets the list item selection in the AutoCompleteTextView's ListView. 1114 */ 1115 public void setListSelection(int index) { 1116 mSearchAutoComplete.setListSelection(index); 1117 } 1118 1119 /** 1120 * When a particular suggestion has been selected, perform the various lookups required 1121 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 1122 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 1123 * the suggestion includes a data id. 1124 * 1125 * @param c The suggestions cursor, moved to the row of the user's selection 1126 * @param actionKey The key code of the action key that was pressed, 1127 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1128 * @param actionMsg The message for the action key that was pressed, 1129 * or <code>null</code> if none. 1130 * @return An intent for the suggestion at the cursor's position. 1131 */ 1132 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { 1133 try { 1134 // use specific action if supplied, or default action if supplied, or fixed default 1135 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 1136 1137 // some items are display only, or have effect via the cursor respond click reporting. 1138 if (SearchManager.INTENT_ACTION_NONE.equals(action)) { 1139 return null; 1140 } 1141 1142 if (action == null) { 1143 action = mSearchable.getSuggestIntentAction(); 1144 } 1145 if (action == null) { 1146 action = Intent.ACTION_SEARCH; 1147 } 1148 1149 // use specific data if supplied, or default data if supplied 1150 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1151 if (data == null) { 1152 data = mSearchable.getSuggestIntentData(); 1153 } 1154 // then, if an ID was provided, append it. 1155 if (data != null) { 1156 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 1157 if (id != null) { 1158 data = data + "/" + Uri.encode(id); 1159 } 1160 } 1161 Uri dataUri = (data == null) ? null : Uri.parse(data); 1162 1163 String componentName = getColumnString( 1164 c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME); 1165 1166 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); 1167 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 1168 1169 return createIntent(action, dataUri, extraData, query, componentName, actionKey, 1170 actionMsg); 1171 } catch (RuntimeException e ) { 1172 int rowNum; 1173 try { // be really paranoid now 1174 rowNum = c.getPosition(); 1175 } catch (RuntimeException e2 ) { 1176 rowNum = -1; 1177 } 1178 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 1179 " returned exception" + e.toString()); 1180 return null; 1181 } 1182 } 1183 1184 /** 1185 * Constructs an intent from the given information and the search dialog state. 1186 * 1187 * @param action Intent action. 1188 * @param data Intent data, or <code>null</code>. 1189 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 1190 * @param query Intent query, or <code>null</code>. 1191 * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>. 1192 * @param actionKey The key code of the action key that was pressed, 1193 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1194 * @param actionMsg The message for the action key that was pressed, 1195 * or <code>null</code> if none. 1196 * @param mode The search mode, one of the acceptable values for 1197 * {@link SearchManager#SEARCH_MODE}, or {@code null}. 1198 * @return The intent. 1199 */ 1200 private Intent createIntent(String action, Uri data, String extraData, String query, 1201 String componentName, int actionKey, String actionMsg) { 1202 // Now build the Intent 1203 Intent intent = new Intent(action); 1204 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1205 // We need CLEAR_TOP to avoid reusing an old task that has other activities 1206 // on top of the one we want. We don't want to do this in in-app search though, 1207 // as it can be destructive to the activity stack. 1208 if (data != null) { 1209 intent.setData(data); 1210 } 1211 intent.putExtra(SearchManager.USER_QUERY, mUserQuery); 1212 if (query != null) { 1213 intent.putExtra(SearchManager.QUERY, query); 1214 } 1215 if (extraData != null) { 1216 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 1217 } 1218 if (mAppSearchData != null) { 1219 intent.putExtra(SearchManager.APP_DATA, mAppSearchData); 1220 } 1221 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 1222 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 1223 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 1224 } 1225 intent.setComponent(mSearchable.getSearchActivity()); 1226 return intent; 1227 } 1228 1229 /** 1230 * For a given suggestion and a given cursor row, get the action message. If not provided 1231 * by the specific row/column, also check for a single definition (for the action key). 1232 * 1233 * @param c The cursor providing suggestions 1234 * @param actionKey The actionkey record being examined 1235 * 1236 * @return Returns a string, or null if no action key message for this suggestion 1237 */ 1238 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) { 1239 String result = null; 1240 // check first in the cursor data, for a suggestion-specific message 1241 final String column = actionKey.getSuggestActionMsgColumn(); 1242 if (column != null) { 1243 result = SuggestionsAdapter.getColumnString(c, column); 1244 } 1245 // If the cursor didn't give us a message, see if there's a single message defined 1246 // for the actionkey (for all suggestions) 1247 if (result == null) { 1248 result = actionKey.getSuggestActionMsg(); 1249 } 1250 return result; 1251 } 1252 1253 /** 1254 * The root element in the search bar layout. This is a custom view just to override 1255 * the handling of the back button. 1256 */ 1257 public static class SearchBar extends LinearLayout { 1258 1259 private SearchDialog mSearchDialog; 1260 1261 public SearchBar(Context context, AttributeSet attrs) { 1262 super(context, attrs); 1263 } 1264 1265 public SearchBar(Context context) { 1266 super(context); 1267 } 1268 1269 public void setSearchDialog(SearchDialog searchDialog) { 1270 mSearchDialog = searchDialog; 1271 } 1272 1273 /** 1274 * Overrides the handling of the back key to move back to the previous sources or dismiss 1275 * the search dialog, instead of dismissing the input method. 1276 */ 1277 @Override 1278 public boolean dispatchKeyEventPreIme(KeyEvent event) { 1279 if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")"); 1280 if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 1281 KeyEvent.DispatcherState state = getKeyDispatcherState(); 1282 if (state != null) { 1283 if (event.getAction() == KeyEvent.ACTION_DOWN 1284 && event.getRepeatCount() == 0) { 1285 state.startTracking(event, this); 1286 return true; 1287 } else if (event.getAction() == KeyEvent.ACTION_UP 1288 && !event.isCanceled() && state.isTracking(event)) { 1289 mSearchDialog.onBackPressed(); 1290 return true; 1291 } 1292 } 1293 } 1294 return super.dispatchKeyEventPreIme(event); 1295 } 1296 } 1297 1298 /** 1299 * Local subclass for AutoCompleteTextView. 1300 */ 1301 public static class SearchAutoComplete extends AutoCompleteTextView { 1302 1303 private int mThreshold; 1304 1305 public SearchAutoComplete(Context context) { 1306 super(context); 1307 mThreshold = getThreshold(); 1308 } 1309 1310 public SearchAutoComplete(Context context, AttributeSet attrs) { 1311 super(context, attrs); 1312 mThreshold = getThreshold(); 1313 } 1314 1315 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1316 super(context, attrs, defStyle); 1317 mThreshold = getThreshold(); 1318 } 1319 1320 @Override 1321 public void setThreshold(int threshold) { 1322 super.setThreshold(threshold); 1323 mThreshold = threshold; 1324 } 1325 1326 /** 1327 * Returns true if the text field is empty, or contains only whitespace. 1328 */ 1329 private boolean isEmpty() { 1330 return TextUtils.getTrimmedLength(getText()) == 0; 1331 } 1332 1333 /** 1334 * We override this method to avoid replacing the query box text 1335 * when a suggestion is clicked. 1336 */ 1337 @Override 1338 protected void replaceText(CharSequence text) { 1339 } 1340 1341 /** 1342 * We override this method to avoid an extra onItemClick being called on the 1343 * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} 1344 * when an item is clicked with the trackball. 1345 */ 1346 @Override 1347 public void performCompletion() { 1348 } 1349 1350 /** 1351 * We override this method to be sure and show the soft keyboard if appropriate when 1352 * the TextView has focus. 1353 */ 1354 @Override 1355 public void onWindowFocusChanged(boolean hasWindowFocus) { 1356 super.onWindowFocusChanged(hasWindowFocus); 1357 1358 if (hasWindowFocus) { 1359 InputMethodManager inputManager = (InputMethodManager) 1360 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 1361 inputManager.showSoftInput(this, 0); 1362 } 1363 } 1364 1365 /** 1366 * We override this method so that we can allow a threshold of zero, which ACTV does not. 1367 */ 1368 @Override 1369 public boolean enoughToFilter() { 1370 return mThreshold <= 0 || super.enoughToFilter(); 1371 } 1372 1373 } 1374 1375 @Override 1376 public void onBackPressed() { 1377 // If the input method is covering the search dialog completely, 1378 // e.g. in landscape mode with no hard keyboard, dismiss just the input method 1379 InputMethodManager imm = (InputMethodManager)getContext() 1380 .getSystemService(Context.INPUT_METHOD_SERVICE); 1381 if (imm != null && imm.isFullscreenMode() && 1382 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) { 1383 return; 1384 } 1385 // Close search dialog 1386 cancel(); 1387 } 1388 1389 /** 1390 * Implements OnItemClickListener 1391 */ 1392 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1393 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); 1394 launchSuggestion(position); 1395 } 1396 1397 /** 1398 * Implements OnItemSelectedListener 1399 */ 1400 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1401 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); 1402 // A suggestion has been selected, rewrite the query if possible, 1403 // otherwise the restore the original query. 1404 if (REWRITE_QUERIES) { 1405 rewriteQueryFromSuggestion(position); 1406 } 1407 } 1408 1409 /** 1410 * Implements OnItemSelectedListener 1411 */ 1412 public void onNothingSelected(AdapterView<?> parent) { 1413 if (DBG) Log.d(LOG_TAG, "onNothingSelected()"); 1414 } 1415 1416 /** 1417 * Query rewriting. 1418 */ 1419 1420 private void rewriteQueryFromSuggestion(int position) { 1421 Cursor c = mSuggestionsAdapter.getCursor(); 1422 if (c == null) { 1423 return; 1424 } 1425 if (c.moveToPosition(position)) { 1426 // Get the new query from the suggestion. 1427 CharSequence newQuery = mSuggestionsAdapter.convertToString(c); 1428 if (newQuery != null) { 1429 // The suggestion rewrites the query. 1430 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'"); 1431 // Update the text field, without getting new suggestions. 1432 setQuery(newQuery); 1433 } else { 1434 // The suggestion does not rewrite the query, restore the user's query. 1435 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query."); 1436 restoreUserQuery(); 1437 } 1438 } else { 1439 // We got a bad position, restore the user's query. 1440 Log.w(LOG_TAG, "Bad suggestion position: " + position); 1441 restoreUserQuery(); 1442 } 1443 } 1444 1445 /** 1446 * Restores the query entered by the user if needed. 1447 */ 1448 private void restoreUserQuery() { 1449 if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'"); 1450 setQuery(mUserQuery); 1451 } 1452 1453 /** 1454 * Sets the text in the query box, without updating the suggestions. 1455 */ 1456 private void setQuery(CharSequence query) { 1457 mSearchAutoComplete.setText(query, false); 1458 if (query != null) { 1459 mSearchAutoComplete.setSelection(query.length()); 1460 } 1461 } 1462 1463 /** 1464 * Sets the text in the query box, updating the suggestions. 1465 */ 1466 private void setUserQuery(String query) { 1467 if (query == null) { 1468 query = ""; 1469 } 1470 mUserQuery = query; 1471 mSearchAutoComplete.setText(query); 1472 mSearchAutoComplete.setSelection(query.length()); 1473 } 1474 1475 /** 1476 * Debugging Support 1477 */ 1478 1479 /** 1480 * For debugging only, sample the millisecond clock and log it. 1481 * Uses AtomicLong so we can use in multiple threads 1482 */ 1483 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis()); 1484 private void dbgLogTiming(final String caller) { 1485 long millis = SystemClock.uptimeMillis(); 1486 long oldTime = mLastLogTime.getAndSet(millis); 1487 long delta = millis - oldTime; 1488 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller; 1489 Log.d(LOG_TAG,report); 1490 } 1491} 1492