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 (mSearchable.getVoiceSearchEnabled() && empty) { 592 Intent testIntent = null; 593 if (mSearchable.getVoiceSearchLaunchWebSearch()) { 594 testIntent = mVoiceWebSearchIntent; 595 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { 596 testIntent = mVoiceAppSearchIntent; 597 } 598 if (testIntent != null) { 599 ResolveInfo ri = getContext().getPackageManager(). 600 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); 601 if (ri != null) { 602 visibility = View.VISIBLE; 603 } 604 } 605 } 606 mVoiceButton.setVisibility(visibility); 607 } 608 609 /** Called by SuggestionsAdapter when the cursor contents changed. */ 610 void onDataSetChanged() { 611 if (mSearchAutoComplete != null && mSuggestionsAdapter != null) { 612 mSearchAutoComplete.onFilterComplete(mSuggestionsAdapter.getCount()); 613 } 614 } 615 616 /** 617 * Hack to determine whether this is the browser, so we can adjust the UI. 618 */ 619 private boolean isBrowserSearch() { 620 return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/"); 621 } 622 623 /* 624 * Listeners of various types 625 */ 626 627 /** 628 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the 629 * touch is outside the window. But the window includes space for the drop-down, 630 * so we also cancel on taps outside the search bar when the drop-down is not showing. 631 */ 632 @Override 633 public boolean onTouchEvent(MotionEvent event) { 634 // cancel if the drop-down is not showing and the touch event was outside the search plate 635 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) { 636 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate."); 637 cancel(); 638 return true; 639 } 640 // Let Dialog handle events outside the window while the pop-up is showing. 641 return super.onTouchEvent(event); 642 } 643 644 private boolean isOutOfBounds(View v, MotionEvent event) { 645 final int x = (int) event.getX(); 646 final int y = (int) event.getY(); 647 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); 648 return (x < -slop) || (y < -slop) 649 || (x > (v.getWidth()+slop)) 650 || (y > (v.getHeight()+slop)); 651 } 652 653 /** 654 * Dialog's OnKeyListener implements various search-specific functionality 655 * 656 * @param keyCode This is the keycode of the typed key, and is the same value as 657 * found in the KeyEvent parameter. 658 * @param event The complete event record for the typed key 659 * 660 * @return Return true if the event was handled here, or false if not. 661 */ 662 @Override 663 public boolean onKeyDown(int keyCode, KeyEvent event) { 664 if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")"); 665 if (mSearchable == null) { 666 return false; 667 } 668 669 // if it's an action specified by the searchable activity, launch the 670 // entered query with the action key 671 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 672 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 673 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 674 return true; 675 } 676 677 return super.onKeyDown(keyCode, event); 678 } 679 680 /** 681 * Callback to watch the textedit field for empty/non-empty 682 */ 683 private TextWatcher mTextWatcher = new TextWatcher() { 684 685 public void beforeTextChanged(CharSequence s, int start, int before, int after) { } 686 687 public void onTextChanged(CharSequence s, int start, 688 int before, int after) { 689 if (DBG_LOG_TIMING) { 690 dbgLogTiming("onTextChanged()"); 691 } 692 if (mSearchable == null) { 693 return; 694 } 695 if (!mSearchAutoComplete.isPerformingCompletion()) { 696 // The user changed the query, remember it. 697 mUserQuery = s == null ? "" : s.toString(); 698 } 699 updateWidgetState(); 700 // Always want to show the microphone if the context is voice. 701 // Also show the microphone if this is a browser search and the 702 // query matches the initial query. 703 updateVoiceButton(mSearchAutoComplete.isEmpty() 704 || (isBrowserSearch() && mInitialQuery.equals(mUserQuery)) 705 || (mAppSearchData != null && mAppSearchData.getBoolean( 706 SearchManager.CONTEXT_IS_VOICE))); 707 } 708 709 public void afterTextChanged(Editable s) { 710 if (mSearchable == null) { 711 return; 712 } 713 if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) { 714 // The user changed the query, check if it is a URL and if so change the search 715 // button in the soft keyboard to the 'Go' button. 716 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)) 717 | EditorInfo.IME_ACTION_GO; 718 if (options != mSearchAutoCompleteImeOptions) { 719 mSearchAutoCompleteImeOptions = options; 720 mSearchAutoComplete.setImeOptions(options); 721 // This call is required to update the soft keyboard UI with latest IME flags. 722 mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType()); 723 } 724 } 725 } 726 }; 727 728 /** 729 * Enable/Disable the go button based on edit text state (any text?) 730 */ 731 private void updateWidgetState() { 732 // enable the button if we have one or more non-space characters 733 boolean enabled = !mSearchAutoComplete.isEmpty(); 734 if (isBrowserSearch()) { 735 // In the browser, we hide the search button when there is no text, 736 // or if the text matches the initial query. 737 if (enabled && !mInitialQuery.equals(mUserQuery)) { 738 mSearchAutoComplete.setBackgroundResource( 739 com.android.internal.R.drawable.textfield_search); 740 mGoButton.setVisibility(View.VISIBLE); 741 // Just to be sure 742 mGoButton.setEnabled(true); 743 mGoButton.setFocusable(true); 744 } else { 745 mSearchAutoComplete.setBackgroundResource( 746 com.android.internal.R.drawable.textfield_search_empty); 747 mGoButton.setVisibility(View.GONE); 748 } 749 } else { 750 // Elsewhere we just disable the button 751 mGoButton.setEnabled(enabled); 752 mGoButton.setFocusable(enabled); 753 } 754 } 755 756 /** 757 * React to typing in the GO search button by refocusing to EditText. 758 * Continue typing the query. 759 */ 760 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() { 761 public boolean onKey(View v, int keyCode, KeyEvent event) { 762 // guard against possible race conditions 763 if (mSearchable == null) { 764 return false; 765 } 766 767 if (!event.isSystem() && 768 (keyCode != KeyEvent.KEYCODE_DPAD_UP) && 769 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) && 770 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) && 771 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) { 772 // restore focus and give key to EditText ... 773 if (mSearchAutoComplete.requestFocus()) { 774 return mSearchAutoComplete.dispatchKeyEvent(event); 775 } 776 } 777 778 return false; 779 } 780 }; 781 782 /** 783 * React to a click in the GO button by launching a search. 784 */ 785 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() { 786 public void onClick(View v) { 787 // guard against possible race conditions 788 if (mSearchable == null) { 789 return; 790 } 791 launchQuerySearch(); 792 } 793 }; 794 795 /** 796 * React to a click in the voice search button. 797 */ 798 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() { 799 public void onClick(View v) { 800 // guard against possible race conditions 801 if (mSearchable == null) { 802 return; 803 } 804 SearchableInfo searchable = mSearchable; 805 try { 806 if (searchable.getVoiceSearchLaunchWebSearch()) { 807 Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, 808 searchable); 809 getContext().startActivity(webSearchIntent); 810 } else if (searchable.getVoiceSearchLaunchRecognizer()) { 811 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, 812 searchable); 813 getContext().startActivity(appSearchIntent); 814 } 815 } catch (ActivityNotFoundException e) { 816 // Should not happen, since we check the availability of 817 // voice search before showing the button. But just in case... 818 Log.w(LOG_TAG, "Could not find voice search activity"); 819 } 820 dismiss(); 821 } 822 }; 823 824 /** 825 * Create and return an Intent that can launch the voice search activity for web search. 826 */ 827 private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { 828 Intent voiceIntent = new Intent(baseIntent); 829 ComponentName searchActivity = searchable.getSearchActivity(); 830 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, 831 searchActivity == null ? null : searchActivity.flattenToShortString()); 832 return voiceIntent; 833 } 834 835 /** 836 * Create and return an Intent that can launch the voice search activity, perform a specific 837 * voice transcription, and forward the results to the searchable activity. 838 * 839 * @param baseIntent The voice app search intent to start from 840 * @return A completely-configured intent ready to send to the voice search activity 841 */ 842 private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { 843 ComponentName searchActivity = searchable.getSearchActivity(); 844 845 // create the necessary intent to set up a search-and-forward operation 846 // in the voice search system. We have to keep the bundle separate, 847 // because it becomes immutable once it enters the PendingIntent 848 Intent queryIntent = new Intent(Intent.ACTION_SEARCH); 849 queryIntent.setComponent(searchActivity); 850 PendingIntent pending = PendingIntent.getActivity( 851 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT); 852 853 // Now set up the bundle that will be inserted into the pending intent 854 // when it's time to do the search. We always build it here (even if empty) 855 // because the voice search activity will always need to insert "QUERY" into 856 // it anyway. 857 Bundle queryExtras = new Bundle(); 858 if (mAppSearchData != null) { 859 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData); 860 } 861 862 // Now build the intent to launch the voice search. Add all necessary 863 // extras to launch the voice recognizer, and then all the necessary extras 864 // to forward the results to the searchable activity 865 Intent voiceIntent = new Intent(baseIntent); 866 867 // Add all of the configuration options supplied by the searchable's metadata 868 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; 869 String prompt = null; 870 String language = null; 871 int maxResults = 1; 872 Resources resources = mActivityContext.getResources(); 873 if (searchable.getVoiceLanguageModeId() != 0) { 874 languageModel = resources.getString(searchable.getVoiceLanguageModeId()); 875 } 876 if (searchable.getVoicePromptTextId() != 0) { 877 prompt = resources.getString(searchable.getVoicePromptTextId()); 878 } 879 if (searchable.getVoiceLanguageId() != 0) { 880 language = resources.getString(searchable.getVoiceLanguageId()); 881 } 882 if (searchable.getVoiceMaxResults() != 0) { 883 maxResults = searchable.getVoiceMaxResults(); 884 } 885 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); 886 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); 887 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); 888 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 889 voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, 890 searchActivity == null ? null : searchActivity.flattenToShortString()); 891 892 // Add the values that configure forwarding the results 893 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); 894 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); 895 896 return voiceIntent; 897 } 898 899 /** 900 * Corrects http/https typo errors in the given url string, and if the protocol specifier was 901 * not present defaults to http. 902 * 903 * @param inUrl URL to check and fix 904 * @return fixed URL string. 905 */ 906 private String fixUrl(String inUrl) { 907 if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) 908 return inUrl; 909 910 if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) { 911 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { 912 inUrl = inUrl.replaceFirst("/", "//"); 913 } else { 914 inUrl = inUrl.replaceFirst(":", "://"); 915 } 916 } 917 918 if (inUrl.indexOf("://") == -1) { 919 inUrl = "http://" + inUrl; 920 } 921 922 return inUrl; 923 } 924 925 /** 926 * React to the user typing "enter" or other hardwired keys while typing in the search box. 927 * This handles these special keys while the edit box has focus. 928 */ 929 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { 930 public boolean onKey(View v, int keyCode, KeyEvent event) { 931 // guard against possible race conditions 932 if (mSearchable == null) { 933 return false; 934 } 935 936 if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()"); 937 if (DBG) { 938 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event 939 + "), selection: " + mSearchAutoComplete.getListSelection()); 940 } 941 942 // If a suggestion is selected, handle enter, search key, and action keys 943 // as presses on the selected suggestion 944 if (mSearchAutoComplete.isPopupShowing() && 945 mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) { 946 return onSuggestionsKey(v, keyCode, event); 947 } 948 949 // If there is text in the query box, handle enter, and action keys 950 // The search key is handled by the dialog's onKeyDown(). 951 if (!mSearchAutoComplete.isEmpty()) { 952 if (keyCode == KeyEvent.KEYCODE_ENTER 953 && event.getAction() == KeyEvent.ACTION_UP) { 954 v.cancelLongPress(); 955 956 // If this is a url entered by the user & we displayed the 'Go' button which 957 // the user clicked, launch the url instead of using it as a search query. 958 if (mSearchable.autoUrlDetect() && 959 (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION) 960 == EditorInfo.IME_ACTION_GO) { 961 Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString())); 962 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 963 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 964 launchIntent(intent); 965 } else { 966 // Launch as a regular search. 967 launchQuerySearch(); 968 } 969 return true; 970 } 971 if (event.getAction() == KeyEvent.ACTION_DOWN) { 972 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 973 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { 974 launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); 975 return true; 976 } 977 } 978 } 979 return false; 980 } 981 }; 982 983 @Override 984 public void hide() { 985 if (!isShowing()) return; 986 987 // We made sure the IME was displayed, so also make sure it is closed 988 // when we go away. 989 InputMethodManager imm = (InputMethodManager)getContext() 990 .getSystemService(Context.INPUT_METHOD_SERVICE); 991 if (imm != null) { 992 imm.hideSoftInputFromWindow( 993 getWindow().getDecorView().getWindowToken(), 0); 994 } 995 996 super.hide(); 997 } 998 999 /** 1000 * React to the user typing while in the suggestions list. First, check for action 1001 * keys. If not handled, try refocusing regular characters into the EditText. 1002 */ 1003 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) { 1004 // guard against possible race conditions (late arrival after dismiss) 1005 if (mSearchable == null) { 1006 return false; 1007 } 1008 if (mSuggestionsAdapter == null) { 1009 return false; 1010 } 1011 if (event.getAction() == KeyEvent.ACTION_DOWN) { 1012 if (DBG_LOG_TIMING) { 1013 dbgLogTiming("onSuggestionsKey()"); 1014 } 1015 1016 // First, check for enter or search (both of which we'll treat as a "click") 1017 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 1018 int position = mSearchAutoComplete.getListSelection(); 1019 return launchSuggestion(position); 1020 } 1021 1022 // Next, check for left/right moves, which we use to "return" the user to the edit view 1023 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 1024 // give "focus" to text editor, with cursor at the beginning if 1025 // left key, at end if right key 1026 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic 1027 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 1028 0 : mSearchAutoComplete.length(); 1029 mSearchAutoComplete.setSelection(selPoint); 1030 mSearchAutoComplete.setListSelection(0); 1031 mSearchAutoComplete.clearListSelection(); 1032 mSearchAutoComplete.ensureImeVisible(true); 1033 1034 return true; 1035 } 1036 1037 // Next, check for an "up and out" move 1038 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 1039 && 0 == mSearchAutoComplete.getListSelection()) { 1040 restoreUserQuery(); 1041 // let ACTV complete the move 1042 return false; 1043 } 1044 1045 // Next, check for an "action key" 1046 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); 1047 if ((actionKey != null) && 1048 ((actionKey.getSuggestActionMsg() != null) || 1049 (actionKey.getSuggestActionMsgColumn() != null))) { 1050 // launch suggestion using action key column 1051 int position = mSearchAutoComplete.getListSelection(); 1052 if (position != ListView.INVALID_POSITION) { 1053 Cursor c = mSuggestionsAdapter.getCursor(); 1054 if (c.moveToPosition(position)) { 1055 final String actionMsg = getActionKeyMessage(c, actionKey); 1056 if (actionMsg != null && (actionMsg.length() > 0)) { 1057 return launchSuggestion(position, keyCode, actionMsg); 1058 } 1059 } 1060 } 1061 } 1062 } 1063 return false; 1064 } 1065 1066 /** 1067 * Launch a search for the text in the query text field. 1068 */ 1069 public void launchQuerySearch() { 1070 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); 1071 } 1072 1073 /** 1074 * Launch a search for the text in the query text field. 1075 * 1076 * @param actionKey The key code of the action key that was pressed, 1077 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1078 * @param actionMsg The message for the action key that was pressed, 1079 * or <code>null</code> if none. 1080 */ 1081 protected void launchQuerySearch(int actionKey, String actionMsg) { 1082 String query = mSearchAutoComplete.getText().toString(); 1083 String action = Intent.ACTION_SEARCH; 1084 Intent intent = createIntent(action, null, null, query, null, 1085 actionKey, actionMsg); 1086 launchIntent(intent); 1087 } 1088 1089 /** 1090 * Launches an intent based on a suggestion. 1091 * 1092 * @param position The index of the suggestion to create the intent from. 1093 * @return true if a successful launch, false if could not (e.g. bad position). 1094 */ 1095 protected boolean launchSuggestion(int position) { 1096 return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); 1097 } 1098 1099 /** 1100 * Launches an intent based on a suggestion. 1101 * 1102 * @param position The index of the suggestion to create the intent from. 1103 * @param actionKey The key code of the action key that was pressed, 1104 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1105 * @param actionMsg The message for the action key that was pressed, 1106 * or <code>null</code> if none. 1107 * @return true if a successful launch, false if could not (e.g. bad position). 1108 */ 1109 protected boolean launchSuggestion(int position, int actionKey, String actionMsg) { 1110 Cursor c = mSuggestionsAdapter.getCursor(); 1111 if ((c != null) && c.moveToPosition(position)) { 1112 1113 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); 1114 1115 // launch the intent 1116 launchIntent(intent); 1117 1118 return true; 1119 } 1120 return false; 1121 } 1122 1123 /** 1124 * Launches an intent, including any special intent handling. 1125 */ 1126 private void launchIntent(Intent intent) { 1127 if (intent == null) { 1128 return; 1129 } 1130 Log.d(LOG_TAG, "launching " + intent); 1131 try { 1132 // If the intent was created from a suggestion, it will always have an explicit 1133 // component here. 1134 Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI()); 1135 getContext().startActivity(intent); 1136 // If the search switches to a different activity, 1137 // SearchDialogWrapper#performActivityResuming 1138 // will handle hiding the dialog when the next activity starts, but for 1139 // real in-app search, we still need to dismiss the dialog. 1140 dismiss(); 1141 } catch (RuntimeException ex) { 1142 Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); 1143 } 1144 } 1145 1146 /** 1147 * If the intent is to open an HTTP or HTTPS URL, we set 1148 * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that 1149 * has been opened by us for the same URL will be reused. 1150 */ 1151 private void setBrowserApplicationId(Intent intent) { 1152 Uri data = intent.getData(); 1153 if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) { 1154 String scheme = data.getScheme(); 1155 if (scheme != null && scheme.startsWith("http")) { 1156 intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString()); 1157 } 1158 } 1159 } 1160 1161 /** 1162 * Sets the list item selection in the AutoCompleteTextView's ListView. 1163 */ 1164 public void setListSelection(int index) { 1165 mSearchAutoComplete.setListSelection(index); 1166 } 1167 1168 /** 1169 * When a particular suggestion has been selected, perform the various lookups required 1170 * to use the suggestion. This includes checking the cursor for suggestion-specific data, 1171 * and/or falling back to the XML for defaults; It also creates REST style Uri data when 1172 * the suggestion includes a data id. 1173 * 1174 * @param c The suggestions cursor, moved to the row of the user's selection 1175 * @param actionKey The key code of the action key that was pressed, 1176 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1177 * @param actionMsg The message for the action key that was pressed, 1178 * or <code>null</code> if none. 1179 * @return An intent for the suggestion at the cursor's position. 1180 */ 1181 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { 1182 try { 1183 // use specific action if supplied, or default action if supplied, or fixed default 1184 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); 1185 1186 // some items are display only, or have effect via the cursor respond click reporting. 1187 if (SearchManager.INTENT_ACTION_NONE.equals(action)) { 1188 return null; 1189 } 1190 1191 if (action == null) { 1192 action = mSearchable.getSuggestIntentAction(); 1193 } 1194 if (action == null) { 1195 action = Intent.ACTION_SEARCH; 1196 } 1197 1198 // use specific data if supplied, or default data if supplied 1199 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1200 if (data == null) { 1201 data = mSearchable.getSuggestIntentData(); 1202 } 1203 // then, if an ID was provided, append it. 1204 if (data != null) { 1205 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 1206 if (id != null) { 1207 data = data + "/" + Uri.encode(id); 1208 } 1209 } 1210 Uri dataUri = (data == null) ? null : Uri.parse(data); 1211 1212 String componentName = getColumnString( 1213 c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME); 1214 1215 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); 1216 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 1217 1218 return createIntent(action, dataUri, extraData, query, componentName, actionKey, 1219 actionMsg); 1220 } catch (RuntimeException e ) { 1221 int rowNum; 1222 try { // be really paranoid now 1223 rowNum = c.getPosition(); 1224 } catch (RuntimeException e2 ) { 1225 rowNum = -1; 1226 } 1227 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 1228 " returned exception" + e.toString()); 1229 return null; 1230 } 1231 } 1232 1233 /** 1234 * Constructs an intent from the given information and the search dialog state. 1235 * 1236 * @param action Intent action. 1237 * @param data Intent data, or <code>null</code>. 1238 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. 1239 * @param query Intent query, or <code>null</code>. 1240 * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>. 1241 * @param actionKey The key code of the action key that was pressed, 1242 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. 1243 * @param actionMsg The message for the action key that was pressed, 1244 * or <code>null</code> if none. 1245 * @param mode The search mode, one of the acceptable values for 1246 * {@link SearchManager#SEARCH_MODE}, or {@code null}. 1247 * @return The intent. 1248 */ 1249 private Intent createIntent(String action, Uri data, String extraData, String query, 1250 String componentName, int actionKey, String actionMsg) { 1251 // Now build the Intent 1252 Intent intent = new Intent(action); 1253 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1254 // We need CLEAR_TOP to avoid reusing an old task that has other activities 1255 // on top of the one we want. We don't want to do this in in-app search though, 1256 // as it can be destructive to the activity stack. 1257 if (data != null) { 1258 intent.setData(data); 1259 } 1260 intent.putExtra(SearchManager.USER_QUERY, mUserQuery); 1261 if (query != null) { 1262 intent.putExtra(SearchManager.QUERY, query); 1263 } 1264 if (extraData != null) { 1265 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 1266 } 1267 if (mAppSearchData != null) { 1268 intent.putExtra(SearchManager.APP_DATA, mAppSearchData); 1269 } 1270 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { 1271 intent.putExtra(SearchManager.ACTION_KEY, actionKey); 1272 intent.putExtra(SearchManager.ACTION_MSG, actionMsg); 1273 } 1274 intent.setComponent(mSearchable.getSearchActivity()); 1275 return intent; 1276 } 1277 1278 /** 1279 * For a given suggestion and a given cursor row, get the action message. If not provided 1280 * by the specific row/column, also check for a single definition (for the action key). 1281 * 1282 * @param c The cursor providing suggestions 1283 * @param actionKey The actionkey record being examined 1284 * 1285 * @return Returns a string, or null if no action key message for this suggestion 1286 */ 1287 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) { 1288 String result = null; 1289 // check first in the cursor data, for a suggestion-specific message 1290 final String column = actionKey.getSuggestActionMsgColumn(); 1291 if (column != null) { 1292 result = SuggestionsAdapter.getColumnString(c, column); 1293 } 1294 // If the cursor didn't give us a message, see if there's a single message defined 1295 // for the actionkey (for all suggestions) 1296 if (result == null) { 1297 result = actionKey.getSuggestActionMsg(); 1298 } 1299 return result; 1300 } 1301 1302 /** 1303 * The root element in the search bar layout. This is a custom view just to override 1304 * the handling of the back button. 1305 */ 1306 public static class SearchBar extends LinearLayout { 1307 1308 private SearchDialog mSearchDialog; 1309 1310 public SearchBar(Context context, AttributeSet attrs) { 1311 super(context, attrs); 1312 } 1313 1314 public SearchBar(Context context) { 1315 super(context); 1316 } 1317 1318 public void setSearchDialog(SearchDialog searchDialog) { 1319 mSearchDialog = searchDialog; 1320 } 1321 1322 /** 1323 * Overrides the handling of the back key to move back to the previous sources or dismiss 1324 * the search dialog, instead of dismissing the input method. 1325 */ 1326 @Override 1327 public boolean dispatchKeyEventPreIme(KeyEvent event) { 1328 if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")"); 1329 if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 1330 KeyEvent.DispatcherState state = getKeyDispatcherState(); 1331 if (state != null) { 1332 if (event.getAction() == KeyEvent.ACTION_DOWN 1333 && event.getRepeatCount() == 0) { 1334 state.startTracking(event, this); 1335 return true; 1336 } else if (event.getAction() == KeyEvent.ACTION_UP 1337 && !event.isCanceled() && state.isTracking(event)) { 1338 mSearchDialog.onBackPressed(); 1339 return true; 1340 } 1341 } 1342 } 1343 return super.dispatchKeyEventPreIme(event); 1344 } 1345 } 1346 1347 /** 1348 * Local subclass for AutoCompleteTextView. 1349 */ 1350 public static class SearchAutoComplete extends AutoCompleteTextView { 1351 1352 private int mThreshold; 1353 1354 public SearchAutoComplete(Context context) { 1355 super(context); 1356 mThreshold = getThreshold(); 1357 } 1358 1359 public SearchAutoComplete(Context context, AttributeSet attrs) { 1360 super(context, attrs); 1361 mThreshold = getThreshold(); 1362 } 1363 1364 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1365 super(context, attrs, defStyle); 1366 mThreshold = getThreshold(); 1367 } 1368 1369 @Override 1370 public void setThreshold(int threshold) { 1371 super.setThreshold(threshold); 1372 mThreshold = threshold; 1373 } 1374 1375 /** 1376 * Returns true if the text field is empty, or contains only whitespace. 1377 */ 1378 private boolean isEmpty() { 1379 return TextUtils.getTrimmedLength(getText()) == 0; 1380 } 1381 1382 /** 1383 * We override this method to avoid replacing the query box text 1384 * when a suggestion is clicked. 1385 */ 1386 @Override 1387 protected void replaceText(CharSequence text) { 1388 } 1389 1390 /** 1391 * We override this method to avoid an extra onItemClick being called on the 1392 * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} 1393 * when an item is clicked with the trackball. 1394 */ 1395 @Override 1396 public void performCompletion() { 1397 } 1398 1399 /** 1400 * We override this method to be sure and show the soft keyboard if appropriate when 1401 * the TextView has focus. 1402 */ 1403 @Override 1404 public void onWindowFocusChanged(boolean hasWindowFocus) { 1405 super.onWindowFocusChanged(hasWindowFocus); 1406 1407 if (hasWindowFocus) { 1408 InputMethodManager inputManager = (InputMethodManager) 1409 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 1410 inputManager.showSoftInput(this, 0); 1411 // If in landscape mode, then make sure that 1412 // the ime is in front of the dropdown. 1413 if (isLandscapeMode(getContext())) { 1414 ensureImeVisible(true); 1415 } 1416 } 1417 } 1418 1419 /** 1420 * We override this method so that we can allow a threshold of zero, which ACTV does not. 1421 */ 1422 @Override 1423 public boolean enoughToFilter() { 1424 return mThreshold <= 0 || super.enoughToFilter(); 1425 } 1426 1427 } 1428 1429 @Override 1430 public void onBackPressed() { 1431 // If the input method is covering the search dialog completely, 1432 // e.g. in landscape mode with no hard keyboard, dismiss just the input method 1433 InputMethodManager imm = (InputMethodManager)getContext() 1434 .getSystemService(Context.INPUT_METHOD_SERVICE); 1435 if (imm != null && imm.isFullscreenMode() && 1436 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) { 1437 return; 1438 } 1439 // Close search dialog 1440 cancel(); 1441 } 1442 1443 /** 1444 * Implements OnItemClickListener 1445 */ 1446 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1447 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); 1448 launchSuggestion(position); 1449 } 1450 1451 /** 1452 * Implements OnItemSelectedListener 1453 */ 1454 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 1455 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); 1456 // A suggestion has been selected, rewrite the query if possible, 1457 // otherwise the restore the original query. 1458 if (REWRITE_QUERIES) { 1459 rewriteQueryFromSuggestion(position); 1460 } 1461 } 1462 1463 /** 1464 * Implements OnItemSelectedListener 1465 */ 1466 public void onNothingSelected(AdapterView<?> parent) { 1467 if (DBG) Log.d(LOG_TAG, "onNothingSelected()"); 1468 } 1469 1470 /** 1471 * Query rewriting. 1472 */ 1473 1474 private void rewriteQueryFromSuggestion(int position) { 1475 Cursor c = mSuggestionsAdapter.getCursor(); 1476 if (c == null) { 1477 return; 1478 } 1479 if (c.moveToPosition(position)) { 1480 // Get the new query from the suggestion. 1481 CharSequence newQuery = mSuggestionsAdapter.convertToString(c); 1482 if (newQuery != null) { 1483 // The suggestion rewrites the query. 1484 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'"); 1485 // Update the text field, without getting new suggestions. 1486 setQuery(newQuery); 1487 } else { 1488 // The suggestion does not rewrite the query, restore the user's query. 1489 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query."); 1490 restoreUserQuery(); 1491 } 1492 } else { 1493 // We got a bad position, restore the user's query. 1494 Log.w(LOG_TAG, "Bad suggestion position: " + position); 1495 restoreUserQuery(); 1496 } 1497 } 1498 1499 /** 1500 * Restores the query entered by the user if needed. 1501 */ 1502 private void restoreUserQuery() { 1503 if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'"); 1504 setQuery(mUserQuery); 1505 } 1506 1507 /** 1508 * Sets the text in the query box, without updating the suggestions. 1509 */ 1510 private void setQuery(CharSequence query) { 1511 mSearchAutoComplete.setText(query, false); 1512 if (query != null) { 1513 mSearchAutoComplete.setSelection(query.length()); 1514 } 1515 } 1516 1517 /** 1518 * Sets the text in the query box, updating the suggestions. 1519 */ 1520 private void setUserQuery(String query) { 1521 if (query == null) { 1522 query = ""; 1523 } 1524 mUserQuery = query; 1525 mSearchAutoComplete.setText(query); 1526 mSearchAutoComplete.setSelection(query.length()); 1527 } 1528 1529 /** 1530 * Debugging Support 1531 */ 1532 1533 /** 1534 * For debugging only, sample the millisecond clock and log it. 1535 * Uses AtomicLong so we can use in multiple threads 1536 */ 1537 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis()); 1538 private void dbgLogTiming(final String caller) { 1539 long millis = SystemClock.uptimeMillis(); 1540 long oldTime = mLastLogTime.getAndSet(millis); 1541 long delta = millis - oldTime; 1542 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller; 1543 Log.d(LOG_TAG,report); 1544 } 1545 } 1546