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