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 */ SearchDialog(Context context, SearchManager searchManager)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 onCreate(Bundle savedInstanceState)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 */ createContentView()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 */ show(String initialQuery, boolean selectInitialQuery, ComponentName componentName, Bundle appSearchData)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 */ doShow(String initialQuery, boolean selectInitialQuery, ComponentName componentName, Bundle appSearchData)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 */ show(ComponentName componentName, Bundle appSearchData)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 onStart()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 onStop()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 */ setWorking(boolean working)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 */ closeSuggestionsAdapter()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 onSaveInstanceState()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 onRestoreInstanceState(Bundle savedInstanceState)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 */ onConfigurationChanged()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 isLandscapeMode(Context context)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 */ updateUI()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 */ updateSearchAutoComplete()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 updateSearchButton()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 updateSearchAppIcon()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 */ updateSearchBadge()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 */ updateQueryHint()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 */ updateVoiceButton(boolean empty)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. */ onDataSetChanged()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 */ isBrowserSearch()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 onTouchEvent(MotionEvent event)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 isOutOfBounds(View v, MotionEvent event)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 onKeyDown(int keyCode, KeyEvent event)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 */ updateWidgetState()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 */ createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable)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 */ createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable)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 */ fixUrl(String inUrl)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 hide()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 */ onSuggestionsKey(View v, int keyCode, KeyEvent event)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 */ launchQuerySearch()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 */ launchQuerySearch(int actionKey, String actionMsg)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 */ launchSuggestion(int position)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 */ launchSuggestion(int position, int actionKey, String actionMsg)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 */ launchIntent(Intent intent)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 */ setBrowserApplicationId(Intent intent)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 */ setListSelection(int index)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 */ createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg)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 */ createIntent(String action, Uri data, String extraData, String query, String componentName, int actionKey, String actionMsg)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 */ getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey)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 SearchBar(Context context, AttributeSet attrs)1312 public SearchBar(Context context, AttributeSet attrs) { 1313 super(context, attrs); 1314 } 1315 SearchBar(Context context)1316 public SearchBar(Context context) { 1317 super(context); 1318 } 1319 setSearchDialog(SearchDialog searchDialog)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 dispatchKeyEventPreIme(KeyEvent event)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 SearchAutoComplete(Context context)1356 public SearchAutoComplete(Context context) { 1357 super(context); 1358 mThreshold = getThreshold(); 1359 } 1360 SearchAutoComplete(Context context, AttributeSet attrs)1361 public SearchAutoComplete(Context context, AttributeSet attrs) { 1362 super(context, attrs); 1363 mThreshold = getThreshold(); 1364 } 1365 SearchAutoComplete(Context context, AttributeSet attrs, int defStyle)1366 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) { 1367 super(context, attrs, defStyle); 1368 mThreshold = getThreshold(); 1369 } 1370 1371 @Override setThreshold(int threshold)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 */ isEmpty()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 replaceText(CharSequence text)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 performCompletion()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 onWindowFocusChanged(boolean hasWindowFocus)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 enoughToFilter()1425 public boolean enoughToFilter() { 1426 return mThreshold <= 0 || super.enoughToFilter(); 1427 } 1428 1429 } 1430 1431 @Override onBackPressed()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 */ onItemClick(AdapterView<?> parent, View view, int position, long id)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 */ onItemSelected(AdapterView<?> parent, View view, int position, long id)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 */ onNothingSelected(AdapterView<?> parent)1468 public void onNothingSelected(AdapterView<?> parent) { 1469 if (DBG) Log.d(LOG_TAG, "onNothingSelected()"); 1470 } 1471 1472 /** 1473 * Query rewriting. 1474 */ 1475 rewriteQueryFromSuggestion(int position)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 */ restoreUserQuery()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 */ setQuery(CharSequence query)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 */ setUserQuery(String query)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()); dbgLogTiming(final String caller)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