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