SearchActivity.java revision 94e8a2be78530170f50e7895a558bf8011bbf8e8
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.quicksearchbox; 18 19import com.android.quicksearchbox.ui.SearchSourceSelector; 20import com.android.quicksearchbox.ui.SuggestionClickListener; 21import com.android.quicksearchbox.ui.SuggestionSelectionListener; 22import com.android.quicksearchbox.ui.SuggestionViewFactory; 23import com.android.quicksearchbox.ui.SuggestionsAdapter; 24import com.android.quicksearchbox.ui.SuggestionsView; 25 26import android.app.Activity; 27import android.app.SearchManager; 28import android.content.ComponentName; 29import android.content.Intent; 30import android.database.DataSetObserver; 31import android.graphics.Rect; 32import android.graphics.drawable.Animatable; 33import android.graphics.drawable.Drawable; 34import android.os.Bundle; 35import android.text.Editable; 36import android.text.TextUtils; 37import android.text.TextWatcher; 38import android.util.Log; 39import android.view.KeyEvent; 40import android.view.Menu; 41import android.view.View; 42import android.view.View.OnFocusChangeListener; 43import android.view.inputmethod.InputMethodManager; 44import android.widget.EditText; 45import android.widget.ImageButton; 46 47/** 48 * The main activity for Quick Search Box. Shows the search UI. 49 * 50 */ 51public class SearchActivity extends Activity { 52 53 private static final boolean DBG = true; 54 private static final String TAG = "QSB.SearchActivity"; 55 56 // TODO: This is hidden in SearchManager 57 public final static String INTENT_ACTION_SEARCH_SETTINGS 58 = "android.search.action.SEARCH_SETTINGS"; 59 60 public static final String EXTRA_KEY_SEARCH_SOURCE 61 = "search_source"; 62 63 // Keys for the saved instance state. 64 private static final String INSTANCE_KEY_SOURCE = "source"; 65 private static final String INSTANCE_KEY_USER_QUERY = "query"; 66 67 protected SuggestionsAdapter mSuggestionsAdapter; 68 69 protected EditText mQueryTextView; 70 71 protected SuggestionsView mSuggestionsView; 72 73 protected ImageButton mSearchGoButton; 74 protected ImageButton mVoiceSearchButton; 75 protected SearchSourceSelector mSourceSelector; 76 77 private Launcher mLauncher; 78 79 private Source mSource; 80 private boolean mUpdateSuggestions; 81 private String mUserQuery; 82 private boolean mSelectAll; 83 84 /** Called when the activity is first created. */ 85 @Override 86 public void onCreate(Bundle savedInstanceState) { 87 if (DBG) Log.d(TAG, "onCreate()"); 88 super.onCreate(savedInstanceState); 89 90 setContentView(R.layout.search_bar); 91 92 mSuggestionsAdapter = getQsbApplication().createSuggestionsAdapter(); 93 94 mQueryTextView = (EditText) findViewById(R.id.search_src_text); 95 mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions); 96 mSuggestionsView.setSuggestionClickListener(new ClickHandler()); 97 mSuggestionsView.setSuggestionSelectionListener(new SelectionHandler()); 98 mSuggestionsView.setInteractionListener(new InputMethodCloser()); 99 mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener()); 100 mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener()); 101 102 mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn); 103 mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn); 104 mSourceSelector = new SearchSourceSelector(findViewById(R.id.search_source_selector)); 105 106 mLauncher = new Launcher(this); 107 // TODO: should this check for voice search in the current source? 108 mVoiceSearchButton.setVisibility( 109 mLauncher.isVoiceSearchAvailable() ? View.VISIBLE : View.GONE); 110 111 mQueryTextView.addTextChangedListener(new SearchTextWatcher()); 112 mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener()); 113 mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener()); 114 115 mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener()); 116 117 mVoiceSearchButton.setOnClickListener(new VoiceSearchButtonClickListener()); 118 119 ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener(); 120 mSearchGoButton.setOnKeyListener(buttonsKeyListener); 121 mVoiceSearchButton.setOnKeyListener(buttonsKeyListener); 122 mSourceSelector.setOnKeyListener(buttonsKeyListener); 123 124 mUpdateSuggestions = true; 125 126 // First get setup from intent 127 Intent intent = getIntent(); 128 setupFromIntent(intent); 129 // Then restore any saved instance state 130 restoreInstanceState(savedInstanceState); 131 132 // Do this at the end, to avoid updating the list view when setSource() 133 // is called. 134 mSuggestionsView.setAdapter(mSuggestionsAdapter); 135 } 136 137 @Override 138 protected void onNewIntent(Intent intent) { 139 setIntent(intent); 140 setupFromIntent(intent); 141 } 142 143 protected void restoreInstanceState(Bundle savedInstanceState) { 144 if (savedInstanceState == null) return; 145 ComponentName sourceName = savedInstanceState.getParcelable(INSTANCE_KEY_SOURCE); 146 String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); 147 setSource(getSourceByComponentName(sourceName)); 148 setUserQuery(query); 149 } 150 151 @Override 152 protected void onSaveInstanceState(Bundle outState) { 153 super.onSaveInstanceState(outState); 154 // We don't save appSearchData, since we always get the value 155 // from the intent and the user can't change it. 156 outState.putParcelable(INSTANCE_KEY_SOURCE, getSourceName()); 157 outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); 158 } 159 160 private void setupFromIntent(Intent intent) { 161 if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")"); 162 if (intent.hasExtra(EXTRA_KEY_SEARCH_SOURCE)) { 163 Source source = getSourceByName(intent.getStringExtra(EXTRA_KEY_SEARCH_SOURCE)); 164 setSource(source); 165 // The source was selected by the user, save it. 166 setLastSelectedSource(source); 167 } else { 168 Source source = getSources().getLastSelectedSource(); 169 if (DBG) Log.d(TAG, "Setting source from preferences: " + source); 170 setSource(source); 171 } 172 setUserQuery(intent.getStringExtra(SearchManager.QUERY)); 173 mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false); 174 setAppSearchData(intent.getBundleExtra(SearchManager.APP_DATA)); 175 } 176 177 private void setLastSelectedSource(Source source) { 178 getSources().setLastSelectedSource(source); 179 // Update search widgets to show the new source. 180 SearchWidgetProvider.updateSearchWidgets(this); 181 } 182 183 private Source getSourceByName(String sourceNameStr) { 184 if (sourceNameStr == null) return null; 185 ComponentName sourceName = ComponentName.unflattenFromString(sourceNameStr); 186 if (sourceName == null) { 187 Log.w(TAG, "Malformed source name: " + sourceName); 188 return null; 189 } 190 return getSourceByComponentName(sourceName); 191 } 192 193 private Source getSourceByComponentName(ComponentName sourceName) { 194 Source source = getSources().getSourceByComponentName(sourceName); 195 if (source == null) { 196 Log.w(TAG, "Unknown source " + sourceName); 197 return null; 198 } 199 return source; 200 } 201 202 private void setSource(Source source) { 203 if (DBG) Log.d(TAG, "setSource(" + source + ")"); 204 mSource = source; 205 Drawable sourceIcon; 206 if (source == null) { 207 sourceIcon = getSuggestionViewFactory().getGlobalSearchIcon(); 208 } else { 209 sourceIcon = source.getSourceIcon(); 210 } 211 ComponentName sourceName = getSourceName(); 212 mSuggestionsAdapter.setSource(sourceName); 213 mSourceSelector.setSource(sourceName); 214 mSourceSelector.setSourceIcon(sourceIcon); 215 } 216 217 private ComponentName getSourceName() { 218 return mSource == null ? null : mSource.getComponentName(); 219 } 220 221 private QsbApplication getQsbApplication() { 222 return (QsbApplication) getApplication(); 223 } 224 225 private SourceLookup getSources() { 226 return getQsbApplication().getSources(); 227 } 228 229 private ShortcutRepository getShortcutRepository() { 230 return getQsbApplication().getShortcutRepository(); 231 } 232 233 private SuggestionsProvider getSuggestionsProvider(Source source) { 234 return getQsbApplication().getSuggestionsProvider(source); 235 } 236 237 private SuggestionViewFactory getSuggestionViewFactory() { 238 return getQsbApplication().getSuggestionViewFactory(); 239 } 240 241 @Override 242 protected void onDestroy() { 243 if (DBG) Log.d(TAG, "onDestroy()"); 244 super.onDestroy(); 245 mSuggestionsView.setAdapter(null); // closes mSuggestionsAdapter 246 } 247 248 @Override 249 protected void onStop() { 250 if (DBG) Log.d(TAG, "onStop()"); 251 // Close all open suggestion cursors. The query will be redone in onResume() 252 // if we come back to this activity. 253 mSuggestionsAdapter.setSuggestions(null); 254 getQsbApplication().getShortcutRefresher().reset(); 255 super.onStop(); 256 } 257 258 @Override 259 protected void onResume() { 260 if (DBG) Log.d(TAG, "onResume()"); 261 super.onResume(); 262 setQuery(mUserQuery, mSelectAll); 263 // Only select everything the first time after creating the activity. 264 mSelectAll = false; 265 updateSuggestions(mUserQuery); 266 mQueryTextView.requestFocus(); 267 } 268 269 @Override 270 public boolean onCreateOptionsMenu(Menu menu) { 271 super.onCreateOptionsMenu(menu); 272 273 Intent settings = new Intent(INTENT_ACTION_SEARCH_SETTINGS); 274 settings.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 275 // Don't show activity chooser if there are multiple search settings activities, 276 // e.g. from different QSB implementations. 277 settings.setPackage(this.getPackageName()); 278 menu.add(Menu.NONE, Menu.NONE, 0, R.string.menu_settings) 279 .setIcon(android.R.drawable.ic_menu_preferences).setAlphabeticShortcut('P') 280 .setIntent(settings); 281 282 return true; 283 } 284 285 /** 286 * Sets the query as typed by the user. Does not update the suggestions 287 * or the text in the query box. 288 */ 289 protected void setUserQuery(String userQuery) { 290 if (userQuery == null) userQuery = ""; 291 mUserQuery = userQuery; 292 mSourceSelector.setQuery(mUserQuery); 293 } 294 295 protected void setAppSearchData(Bundle appSearchData) { 296 mLauncher.setAppSearchData(appSearchData); 297 mSourceSelector.setAppSearchData(appSearchData); 298 } 299 300 protected String getQuery() { 301 CharSequence q = mQueryTextView.getText(); 302 return q == null ? "" : q.toString(); 303 } 304 305 /** 306 * Restores the query entered by the user. 307 */ 308 private void restoreUserQuery() { 309 if (DBG) Log.d(TAG, "Restoring query to '" + mUserQuery + "'"); 310 setQuery(mUserQuery, false); 311 } 312 313 /** 314 * Sets the text in the query box. Does not update the suggestions, 315 * and does not change the saved user-entered query. 316 * {@link #restoreUserQuery()} will restore the query to the last 317 * user-entered query. 318 */ 319 private void setQuery(String query, boolean selectAll) { 320 mUpdateSuggestions = false; 321 mQueryTextView.setText(query); 322 setTextSelection(selectAll); 323 mUpdateSuggestions = true; 324 } 325 326 /** 327 * Sets the text selection in the query text view. 328 * 329 * @param selectAll If {@code true}, selects the entire query. 330 * If {@false}, no characters are selected, and the cursor is placed 331 * at the end of the query. 332 */ 333 private void setTextSelection(boolean selectAll) { 334 if (selectAll) { 335 mQueryTextView.setSelection(0, mQueryTextView.length()); 336 } else { 337 mQueryTextView.setSelection(mQueryTextView.length()); 338 } 339 } 340 341 protected void onSearchClicked() { 342 String query = getQuery(); 343 if (DBG) Log.d(TAG, "Search clicked, query=" + query); 344 mLauncher.startSearch(mSource, query); 345 } 346 347 protected void onVoiceSearchClicked() { 348 if (DBG) Log.d(TAG, "Voice Search clicked"); 349 // TODO: should this start voice search in the current source? 350 mLauncher.startVoiceSearch(); 351 } 352 353 protected boolean launchSuggestion(SuggestionPosition suggestion) { 354 return launchSuggestion(suggestion, KeyEvent.KEYCODE_UNKNOWN, null); 355 } 356 357 protected boolean launchSuggestion(SuggestionPosition suggestion, 358 int actionKey, String actionMsg) { 359 if (DBG) Log.d(TAG, "Launching suggestion " + suggestion); 360 mLauncher.launchSuggestion(suggestion, actionKey, actionMsg); 361 getShortcutRepository().reportClick(suggestion); 362 // Update search widgets, since the top shortcuts can have changed. 363 SearchWidgetProvider.updateSearchWidgets(this); 364 return true; 365 } 366 367 protected boolean launchSuggestionSecondary(SuggestionPosition suggestion, Rect target) { 368 if (DBG) Log.d(TAG, "Clicked on suggestion icon " + suggestion); 369 mLauncher.launchSuggestionSecondary(suggestion, target); 370 getShortcutRepository().reportClick(suggestion); 371 return true; 372 } 373 374 protected boolean onSuggestionLongClicked(SuggestionPosition suggestion) { 375 SuggestionCursor sourceResult = suggestion.getSuggestion(); 376 if (DBG) Log.d(TAG, "Long clicked on suggestion " + sourceResult.getSuggestionText1()); 377 return false; 378 } 379 380 protected void onSuggestionSelected(SuggestionPosition suggestion) { 381 if (suggestion == null) { 382 // This happens when a suggestion has been selected with the 383 // dpad / trackball and then a different UI element is touched. 384 // Do nothing, since we want to keep the query of the selection 385 // in the search box. 386 return; 387 } 388 SuggestionCursor sourceResult = suggestion.getSuggestion(); 389 String displayQuery = sourceResult.getSuggestionDisplayQuery(); 390 if (DBG) { 391 Log.d(TAG, "Selected suggestion " + sourceResult.getSuggestionText1() 392 + ",displayQuery="+ displayQuery); 393 } 394 if (TextUtils.isEmpty(displayQuery)) { 395 restoreUserQuery(); 396 } else { 397 setQuery(displayQuery, false); 398 } 399 } 400 401 protected boolean onSuggestionKeyDown(SuggestionPosition suggestion, 402 int keyCode, KeyEvent event) { 403 // Treat enter or search as a click 404 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) { 405 return launchSuggestion(suggestion); 406 } 407 408 if (keyCode == KeyEvent.KEYCODE_DPAD_UP 409 && mSuggestionsView.getSelectedItemPosition() == 0) { 410 // Moved up from the top suggestion, restore the user query and focus query box 411 if (DBG) Log.d(TAG, "Up and out"); 412 restoreUserQuery(); 413 return false; // let the framework handle the move 414 } 415 416 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT 417 || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 418 // Moved left / right from a suggestion, keep current query, move 419 // focus to query box, and move cursor to far left / right 420 if (DBG) Log.d(TAG, "Left/right on a suggestion"); 421 int cursorPos = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mQueryTextView.length(); 422 mQueryTextView.setSelection(cursorPos); 423 mQueryTextView.requestFocus(); 424 // TODO: should we modify the list selection? 425 return true; 426 } 427 428 // Handle source-specified action keys 429 String actionMsg = suggestion.getSuggestion().getActionKeyMsg(keyCode); 430 if (actionMsg != null) { 431 return launchSuggestion(suggestion, keyCode, actionMsg); 432 } 433 434 return false; 435 } 436 437 protected void onSourceSelected() { 438 if (DBG) Log.d(TAG, "No suggestion selected"); 439 restoreUserQuery(); 440 } 441 442 protected int getSelectedPosition() { 443 return mSuggestionsView.getSelectedPosition(); 444 } 445 446 protected SuggestionPosition getSelectedSuggestion() { 447 return mSuggestionsView.getSelectedSuggestion(); 448 } 449 450 /** 451 * Hides the input method. 452 */ 453 protected void hideInputMethod() { 454 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 455 if (imm != null) { 456 imm.hideSoftInputFromWindow(mQueryTextView.getWindowToken(), 0); 457 } 458 } 459 460 protected void showInputMethodForQuery() { 461 InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 462 if (imm != null) { 463 imm.showSoftInput(mQueryTextView, 0); 464 } 465 } 466 467 /** 468 * Hides the input method when the suggestions get focus. 469 */ 470 private class SuggestListFocusListener implements OnFocusChangeListener { 471 public void onFocusChange(View v, boolean focused) { 472 if (DBG) Log.d(TAG, "Suggestions focus change, now: " + focused); 473 if (focused) { 474 // The suggestions list got focus, hide the input method 475 hideInputMethod(); 476 } 477 } 478 } 479 480 private class QueryTextViewFocusListener implements OnFocusChangeListener { 481 public void onFocusChange(View v, boolean focused) { 482 if (DBG) Log.d(TAG, "Query focus change, now: " + focused); 483 if (focused) { 484 // The query box got focus, show the input method if the 485 // query box got focus? 486 showInputMethodForQuery(); 487 } 488 } 489 } 490 491 private void startSearchProgress() { 492 // TODO: Cache animation between calls? 493 mSearchGoButton.setImageResource(R.drawable.searching); 494 Animatable animation = (Animatable) mSearchGoButton.getDrawable(); 495 animation.start(); 496 } 497 498 private void stopSearchProgress() { 499 Drawable animation = mSearchGoButton.getDrawable(); 500 if (animation instanceof Animatable) { 501 // TODO: Is this needed, or is it done automatically when the 502 // animation is removed? 503 ((Animatable) animation).stop(); 504 } 505 mSearchGoButton.setImageResource(R.drawable.ic_btn_search); 506 } 507 508 private void updateSuggestions(String query) { 509 LatencyTracker latency = new LatencyTracker(TAG); 510 Suggestions suggestions = getSuggestionsProvider(mSource).getSuggestions(query); 511 latency.addEvent("getSuggestions_done"); 512 if (!suggestions.isDone()) { 513 suggestions.registerDataSetObserver(new ProgressUpdater(suggestions)); 514 startSearchProgress(); 515 } else { 516 stopSearchProgress(); 517 } 518 mSuggestionsAdapter.setSuggestions(suggestions); 519 latency.addEvent("shortcuts_shown"); 520 long userVisibleLatency = latency.getUserVisibleLatency(); 521 if (DBG) { 522 Log.d(TAG, "User visible latency (shortcuts): " + userVisibleLatency + " ms."); 523 } 524 } 525 526 private boolean forwardKeyToQueryTextView(int keyCode, KeyEvent event) { 527 if (!event.isSystem() && !isDpadKey(keyCode)) { 528 if (DBG) Log.d(TAG, "Forwarding key to query box: " + event); 529 if (mQueryTextView.requestFocus()) { 530 return mQueryTextView.dispatchKeyEvent(event); 531 } 532 } 533 return false; 534 } 535 536 private boolean isDpadKey(int keyCode) { 537 switch (keyCode) { 538 case KeyEvent.KEYCODE_DPAD_UP: 539 case KeyEvent.KEYCODE_DPAD_DOWN: 540 case KeyEvent.KEYCODE_DPAD_LEFT: 541 case KeyEvent.KEYCODE_DPAD_RIGHT: 542 case KeyEvent.KEYCODE_DPAD_CENTER: 543 return true; 544 default: 545 return false; 546 } 547 } 548 549 /** 550 * Filters the suggestions list when the search text changes. 551 */ 552 private class SearchTextWatcher implements TextWatcher { 553 public void afterTextChanged(Editable s) { 554 if (mUpdateSuggestions) { 555 String query = s == null ? "" : s.toString(); 556 setUserQuery(query); 557 updateSuggestions(query); 558 } 559 } 560 561 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 562 } 563 564 public void onTextChanged(CharSequence s, int start, int before, int count) { 565 } 566 } 567 568 /** 569 * Handles non-text keys in the query text view. 570 */ 571 private class QueryTextViewKeyListener implements View.OnKeyListener { 572 public boolean onKey(View view, int keyCode, KeyEvent event) { 573 // Handle IME search action key 574 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 575 onSearchClicked(); 576 } 577 return false; 578 } 579 } 580 581 /** 582 * Handles key events on the search and voice search buttons, 583 * by refocusing to EditText. 584 */ 585 private class ButtonsKeyListener implements View.OnKeyListener { 586 public boolean onKey(View v, int keyCode, KeyEvent event) { 587 return forwardKeyToQueryTextView(keyCode, event); 588 } 589 } 590 591 /** 592 * Handles key events on the suggestions list view. 593 */ 594 private class SuggestionsViewKeyListener implements View.OnKeyListener { 595 public boolean onKey(View v, int keyCode, KeyEvent event) { 596 if (event.getAction() == KeyEvent.ACTION_DOWN) { 597 SuggestionPosition suggestion = getSelectedSuggestion(); 598 if (suggestion != null) { 599 if (onSuggestionKeyDown(suggestion, keyCode, event)) { 600 return true; 601 } 602 } 603 } 604 return forwardKeyToQueryTextView(keyCode, event); 605 } 606 } 607 608 private class InputMethodCloser implements SuggestionsView.InteractionListener { 609 public void onInteraction() { 610 hideInputMethod(); 611 } 612 } 613 614 private class ClickHandler implements SuggestionClickListener { 615 public void onSuggestionClicked(SuggestionPosition suggestion) { 616 launchSuggestion(suggestion); 617 } 618 619 public boolean onSuggestionLongClicked(SuggestionPosition suggestion) { 620 return SearchActivity.this.onSuggestionLongClicked(suggestion); 621 } 622 623 public void onSuggestionIconClicked(SuggestionPosition suggestion, Rect rect) { 624 launchSuggestionSecondary(suggestion, rect); 625 } 626 } 627 628 private class SelectionHandler implements SuggestionSelectionListener { 629 public void onSelectionChanged(SuggestionPosition suggestion) { 630 onSuggestionSelected(suggestion); 631 } 632 } 633 634 /** 635 * Listens for clicks on the search button. 636 */ 637 private class SearchGoButtonClickListener implements View.OnClickListener { 638 public void onClick(View view) { 639 onSearchClicked(); 640 } 641 } 642 643 /** 644 * Listens for clicks on the voice search button. 645 */ 646 private class VoiceSearchButtonClickListener implements View.OnClickListener { 647 public void onClick(View view) { 648 onVoiceSearchClicked(); 649 } 650 } 651 652 /** 653 * Updates the progress bar when the suggestions adapter changes its progress. 654 */ 655 private class ProgressUpdater extends DataSetObserver { 656 private final Suggestions mSuggestions; 657 658 public ProgressUpdater(Suggestions suggestions) { 659 mSuggestions = suggestions; 660 } 661 662 @Override 663 public void onChanged() { 664 if (mSuggestions.isDone()) { 665 stopSearchProgress(); 666 } 667 } 668 } 669 670} 671