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