FileDocCategorySizeDatePackage
SearchDialog.javaAPI DocAndroid 1.5 API68515Wed May 06 22:41:54 BST 2009android.app

SearchDialog.java

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.app;

import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.server.search.SearchableInfo;
import android.speech.RecognizerIntent;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CursorAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.WrapperListAdapter;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;

import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicLong;

/**
 * In-application-process implementation of Search Bar.  This is still controlled by the 
 * SearchManager, but it runs in the current activity's process to keep things lighter weight.
 * 
 * @hide
 */
public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {

    // Debugging support
    final static String LOG_TAG = "SearchDialog";
    private static final int DBG_LOG_TIMING = 0;
    final static int DBG_JAM_THREADING = 0;

    // interaction with runtime
    IntentFilter mCloseDialogsFilter;
    IntentFilter mPackageFilter;
    
    private static final String INSTANCE_KEY_COMPONENT = "comp";
    private static final String INSTANCE_KEY_APPDATA = "data";
    private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
    private static final String INSTANCE_KEY_DISPLAY_QUERY = "dQry";
    private static final String INSTANCE_KEY_DISPLAY_SEL_START = "sel1";
    private static final String INSTANCE_KEY_DISPLAY_SEL_END = "sel2";
    private static final String INSTANCE_KEY_USER_QUERY = "uQry";
    private static final String INSTANCE_KEY_SUGGESTION_QUERY = "sQry";
    private static final String INSTANCE_KEY_SELECTED_ELEMENT = "slEl";
    private static final int INSTANCE_SELECTED_BUTTON = -2;
    private static final int INSTANCE_SELECTED_QUERY = -1;

    // views & widgets
    private TextView mBadgeLabel;
    private AutoCompleteTextView mSearchTextField;
    private Button mGoButton;
    private ImageButton mVoiceButton;

    // interaction with searchable application
    private ComponentName mLaunchComponent;
    private Bundle mAppSearchData;
    private boolean mGlobalSearchMode;
    private Context mActivityContext;

    // interaction with the search manager service
    private SearchableInfo mSearchable;
    
    // support for suggestions 
    private String mUserQuery = null;
    private int mUserQuerySelStart;
    private int mUserQuerySelEnd;
    private boolean mLeaveJammedQueryOnRefocus = false;
    private String mPreviousSuggestionQuery = null;
    private int mPresetSelection = -1;
    private String mSuggestionAction = null;
    private Uri mSuggestionData = null;
    private String mSuggestionQuery = null;
    
    // For voice searching
    private Intent mVoiceWebSearchIntent;
    private Intent mVoiceAppSearchIntent;

    // support for AutoCompleteTextView suggestions display
    private SuggestionsAdapter mSuggestionsAdapter;

    /**
     * Constructor - fires it up and makes it look like the search UI.
     * 
     * @param context Application Context we can use for system acess
     */
    public SearchDialog(Context context) {
        super(context, com.android.internal.R.style.Theme_SearchBar);
    }

    /**
     * We create the search dialog just once, and it stays around (hidden)
     * until activated by the user.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Window theWindow = getWindow();
        theWindow.setGravity(Gravity.TOP|Gravity.FILL_HORIZONTAL);

        setContentView(com.android.internal.R.layout.search_bar);

        theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        WindowManager.LayoutParams lp = theWindow.getAttributes();
        lp.setTitle("Search Dialog");
        lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
        theWindow.setAttributes(lp);

        // get the view elements for local access
        mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
        mSearchTextField = (AutoCompleteTextView) 
                findViewById(com.android.internal.R.id.search_src_text);
        mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
        mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
        
        // attach listeners
        mSearchTextField.addTextChangedListener(mTextWatcher);
        mSearchTextField.setOnKeyListener(mTextKeyListener);
        mGoButton.setOnClickListener(mGoButtonClickListener);
        mGoButton.setOnKeyListener(mButtonsKeyListener);
        mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
        mVoiceButton.setOnKeyListener(mButtonsKeyListener);

        // pre-hide all the extraneous elements
        mBadgeLabel.setVisibility(View.GONE);

        // Additional adjustments to make Dialog work for Search

        // Touching outside of the search dialog will dismiss it 
        setCanceledOnTouchOutside(true);
        
        // Set up broadcast filters
        mCloseDialogsFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        mPackageFilter = new IntentFilter();
        mPackageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
        mPackageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        mPackageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        mPackageFilter.addDataScheme("package");
        
        // Save voice intent for later queries/launching
        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
        
        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    }

    /**
     * Set up the search dialog
     * 
     * @param Returns true if search dialog launched, false if not
     */
    public boolean show(String initialQuery, boolean selectInitialQuery,
            ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
        if (isShowing()) {
            // race condition - already showing but not handling events yet.
            // in this case, just discard the "show" request
            return true;
        }

        // Get searchable info from search manager and use to set up other elements of UI
        // Do this first so we can get out quickly if there's nothing to search
        ISearchManager sms;
        sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE));
        try {
            mSearchable = sms.getSearchableInfo(componentName, globalSearch);
        } catch (RemoteException e) {
            mSearchable = null;
        }
        if (mSearchable == null) {
            // unfortunately, we can't log here.  it would be logspam every time the user
            // clicks the "search" key on a non-search app
            return false;
        }
        
        // OK, we're going to show ourselves
        super.show();

        setupSearchableInfo();
        
        mLaunchComponent = componentName;
        mAppSearchData = appSearchData;
        mGlobalSearchMode = globalSearch;

        // receive broadcasts
        getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
        getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
        
        // configure the autocomplete aspects of the input box
        mSearchTextField.setOnItemClickListener(this);
        mSearchTextField.setOnItemSelectedListener(this);

        // This conversion is necessary to force a preload of the EditText and thus force
        // suggestions to be presented (even for an empty query)
        if (initialQuery == null) {
            initialQuery = "";     // This forces the preload to happen, triggering suggestions
        }

        // attach the suggestions adapter, if suggestions are available
        // The existence of a suggestions authority is the proxy for "suggestions available here"
        if (mSearchable.getSuggestAuthority() == null) {
            mSuggestionsAdapter = null;
            mSearchTextField.setAdapter(mSuggestionsAdapter);
            mSearchTextField.setText(initialQuery);
        } else {
            mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable, 
                    mSearchTextField);
            mSearchTextField.setAdapter(mSuggestionsAdapter);

            // finally, load the user's initial text (which may trigger suggestions)
            mSuggestionsAdapter.setNonUserQuery(false);
            mSearchTextField.setText(initialQuery);
        }
        
        if (selectInitialQuery) {
            mSearchTextField.selectAll();
        } else {
            mSearchTextField.setSelection(initialQuery.length());
        }
        return true;
    }

    /**
     * The default show() for this Dialog is not supported.
     */
    @Override
    public void show() {
        return;
    }

    /**
     * The search dialog is being dismissed, so handle all of the local shutdown operations.
     * 
     * This function is designed to be idempotent so that dismiss() can be safely called at any time
     * (even if already closed) and more likely to really dump any memory.  No leaks!
     */
    @Override
    public void onStop() {
        super.onStop();
        
        setOnCancelListener(null);
        setOnDismissListener(null);
        
        // stop receiving broadcasts (throws exception if none registered)
        try {
            getContext().unregisterReceiver(mBroadcastReceiver);
        } catch (RuntimeException e) {
            // This is OK - it just means we didn't have any registered
        }
        
        // close any leftover cursor
        if (mSuggestionsAdapter != null) {
            mSuggestionsAdapter.changeCursor(null);
        }
        
        // dump extra memory we're hanging on to
        mLaunchComponent = null;
        mAppSearchData = null;
        mSearchable = null;
        mSuggestionAction = null;
        mSuggestionData = null;
        mSuggestionQuery = null;
        mActivityContext = null;
        mPreviousSuggestionQuery = null;
        mUserQuery = null;
    }
    
    /**
     * Save the minimal set of data necessary to recreate the search
     * 
     * @return A bundle with the state of the dialog.
     */
    @Override
    public Bundle onSaveInstanceState() {
        Bundle bundle = new Bundle();
        
        // setup info so I can recreate this particular search       
        bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
        bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
        bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
        
        // UI state
        bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchTextField.getText().toString());
        bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchTextField.getSelectionStart());
        bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchTextField.getSelectionEnd());
        bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
        bundle.putString(INSTANCE_KEY_SUGGESTION_QUERY, mPreviousSuggestionQuery);
        
        int selectedElement = INSTANCE_SELECTED_QUERY;
        if (mGoButton.isFocused()) {
            selectedElement = INSTANCE_SELECTED_BUTTON;
        } else if (mSearchTextField.isPopupShowing()) {
            selectedElement = 0; // TODO mSearchTextField.getListSelection()    // 0..n
        }
        bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement);
        
        return bundle;
    }

    /**
     * Restore the state of the dialog from a previously saved bundle.
     *
     * @param savedInstanceState The state of the dialog previously saved by
     *     {@link #onSaveInstanceState()}.
     */
    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState) {
        // Get the launch info
        ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
        Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
        boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
        
        // get the UI state
        String displayQuery = savedInstanceState.getString(INSTANCE_KEY_DISPLAY_QUERY);
        int querySelStart = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_START, -1);
        int querySelEnd = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_END, -1);
        String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
        int selectedElement = savedInstanceState.getInt(INSTANCE_KEY_SELECTED_ELEMENT);
        String suggestionQuery = savedInstanceState.getString(INSTANCE_KEY_SUGGESTION_QUERY);
        
        // show the dialog.  skip any show/hide animation, we want to go fast.
        // send the text that actually generates the suggestions here;  we'll replace the display
        // text as necessary in a moment.
        if (!show(suggestionQuery, false, launchComponent, appSearchData, globalSearch)) {
            // for some reason, we couldn't re-instantiate
            return;
        }
        
        if (mSuggestionsAdapter != null) {
            mSuggestionsAdapter.setNonUserQuery(true);
        }
        mSearchTextField.setText(displayQuery);
        // TODO because the new query is (not) processed in another thread, we can't just
        // take away this flag (yet).  The better solution here is going to require a new API
        // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
//      mSuggestionsAdapter.setNonUserQuery(false);
        
        // clean up the selection state
        switch (selectedElement) {
        case INSTANCE_SELECTED_BUTTON:
            mGoButton.setEnabled(true);
            mGoButton.setFocusable(true);
            mGoButton.requestFocus();
            break;
        case INSTANCE_SELECTED_QUERY:
            if (querySelStart >= 0 && querySelEnd >= 0) {
                mSearchTextField.requestFocus();
                mSearchTextField.setSelection(querySelStart, querySelEnd);
            }
            break;
        default:
            // defer selecting a list element until suggestion list appears
            mPresetSelection = selectedElement;
            // TODO mSearchTextField.setListSelection(selectedElement)
            break;
        }
    }
    
    /**
     * Hook for updating layout on a rotation
     * 
     */
    public void onConfigurationChanged(Configuration newConfig) {
        if (isShowing()) {
            // Redraw (resources may have changed)
            updateSearchButton();
            updateSearchBadge();
            updateQueryHint();
        } 
    }

    /**
     * Use SearchableInfo record (from search manager service) to preconfigure the UI in various
     * ways.
     */
    private void setupSearchableInfo() {
        if (mSearchable != null) {
            mActivityContext = mSearchable.getActivityContext(getContext());
            
            updateSearchButton();
            updateSearchBadge();
            updateQueryHint();
            updateVoiceButton();
            
            // In order to properly configure the input method (if one is being used), we
            // need to let it know if we'll be providing suggestions.  Although it would be
            // difficult/expensive to know if every last detail has been configured properly, we 
            // can at least see if a suggestions provider has been configured, and use that
            // as our trigger.
            int inputType = mSearchable.getInputType();
            // We only touch this if the input type is set up for text (which it almost certainly
            // should be, in the case of search!)
            if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
                // The existence of a suggestions authority is the proxy for "suggestions 
                // are available here"
                inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
                if (mSearchable.getSuggestAuthority() != null) {
                    inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
                }
            }
            mSearchTextField.setInputType(inputType);
            mSearchTextField.setImeOptions(mSearchable.getImeOptions());
        }
    }

    /**
     * The list of installed packages has just changed.  This means that our current context
     * may no longer be valid.  This would only happen if a package is installed/removed exactly
     * when the search bar is open.  So for now we're just going to close the search
     * bar.  
     * 
     * Anything fancier would require some checks to see if the user's context was still valid.
     * Which would be messier.
     */
    public void onPackageListChange() {
        cancel();
    }
    
    /**    
     * Update the text in the search button.  Note: This is deprecated functionality, for 
     * 1.0 compatibility only.
     */  
    private void updateSearchButton() { 
        String textLabel = null;
        Drawable iconLabel = null;
        int textId = mSearchable.getSearchButtonText(); 
        if (textId != 0) {
            textLabel = mActivityContext.getResources().getString(textId);  
        } else {
            iconLabel = getContext().getResources().
                    getDrawable(com.android.internal.R.drawable.ic_btn_search);
        }
        mGoButton.setText(textLabel);  
        mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
    }
    
    /**
     * Setup the search "Badge" if request by mode flags.
     */
    private void updateSearchBadge() {
        // assume both hidden
        int visibility = View.GONE;
        Drawable icon = null;
        String text = null;
        
        // optionally show one or the other.
        if (mSearchable.mBadgeIcon) {
            icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
            visibility = View.VISIBLE;
        } else if (mSearchable.mBadgeLabel) {
            text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
            visibility = View.VISIBLE;
        }
        
        mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
        mBadgeLabel.setText(text);
        mBadgeLabel.setVisibility(visibility);
    }

    /**
     * Update the hint in the query text field.
     */
    private void updateQueryHint() {
        if (isShowing()) {
            String hint = null;
            if (mSearchable != null) {
                int hintId = mSearchable.getHintId();
                if (hintId != 0) {
                    hint = mActivityContext.getString(hintId);
                }
            }
            mSearchTextField.setHint(hint);
        }
    }

    /**
     * Update the visibility of the voice button.  There are actually two voice search modes, 
     * either of which will activate the button.
     */
    private void updateVoiceButton() {
        int visibility = View.GONE;
        if (mSearchable.getVoiceSearchEnabled()) {
            Intent testIntent = null;
            if (mSearchable.getVoiceSearchLaunchWebSearch()) {
                testIntent = mVoiceWebSearchIntent;
            } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
                testIntent = mVoiceAppSearchIntent;
            }      
            if (testIntent != null) {
                ResolveInfo ri = getContext().getPackageManager().
                        resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
                if (ri != null) {
                    visibility = View.VISIBLE;
                }
            }
        }
        mVoiceButton.setVisibility(visibility);
    }
    
    /**
     * Listeners of various types
     */

    /**
     * Dialog's OnKeyListener implements various search-specific functionality
     *
     * @param keyCode This is the keycode of the typed key, and is the same value as
     * found in the KeyEvent parameter.
     * @param event The complete event record for the typed key
     *
     * @return Return true if the event was handled here, or false if not.
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
        case KeyEvent.KEYCODE_BACK:
            cancel();
            return true;
        case KeyEvent.KEYCODE_SEARCH:
            if (TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0) {
                launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
            } else {
                cancel();
            }
            return true;
        default:
            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
            if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
                launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
                return true;
            }
            break;
        }
        return false;
    }

    /**
     * Callback to watch the textedit field for empty/non-empty
     */
    private TextWatcher mTextWatcher = new TextWatcher() {

        public void beforeTextChanged(CharSequence s, int start, int
                before, int after) { }

        public void onTextChanged(CharSequence s, int start,
                int before, int after) {
            if (DBG_LOG_TIMING == 1) {
                dbgLogTiming("onTextChanged()");
            }
            updateWidgetState();
            // Only do suggestions if actually typed by user
            if ((mSuggestionsAdapter != null) && !mSuggestionsAdapter.getNonUserQuery()) {
                mPreviousSuggestionQuery = s.toString();
                mUserQuery = mSearchTextField.getText().toString();
                mUserQuerySelStart = mSearchTextField.getSelectionStart();
                mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
            }
        }

        public void afterTextChanged(Editable s) { }
    };

    /**
     * Enable/Disable the cancel button based on edit text state (any text?)
     */
    private void updateWidgetState() {
        // enable the button if we have one or more non-space characters
        boolean enabled =
            TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0;

        mGoButton.setEnabled(enabled);
        mGoButton.setFocusable(enabled);
    }

    private final static String[] ONE_LINE_FROM =       {SearchManager.SUGGEST_COLUMN_TEXT_1 };
    private final static String[] ONE_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
                                                         SearchManager.SUGGEST_COLUMN_ICON_1,
                                                         SearchManager.SUGGEST_COLUMN_ICON_2};
    private final static String[] TWO_LINE_FROM =       {SearchManager.SUGGEST_COLUMN_TEXT_1,
                                                         SearchManager.SUGGEST_COLUMN_TEXT_2 };
    private final static String[] TWO_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
                                                         SearchManager.SUGGEST_COLUMN_TEXT_2,
                                                         SearchManager.SUGGEST_COLUMN_ICON_1,
                                                         SearchManager.SUGGEST_COLUMN_ICON_2 };
    
    private final static int[] ONE_LINE_TO =       {com.android.internal.R.id.text1};
    private final static int[] ONE_LINE_ICONS_TO = {com.android.internal.R.id.text1,
                                                    com.android.internal.R.id.icon1, 
                                                    com.android.internal.R.id.icon2};
    private final static int[] TWO_LINE_TO =       {com.android.internal.R.id.text1, 
                                                    com.android.internal.R.id.text2};
    private final static int[] TWO_LINE_ICONS_TO = {com.android.internal.R.id.text1, 
                                                    com.android.internal.R.id.text2,
                                                    com.android.internal.R.id.icon1, 
                                                    com.android.internal.R.id.icon2};
    
    /**
     * Safely retrieve the suggestions cursor adapter from the ListView
     * 
     * @param adapterView The ListView containing our adapter
     * @result The CursorAdapter that we installed, or null if not set
     */
    private static CursorAdapter getSuggestionsAdapter(AdapterView<?> adapterView) {
        CursorAdapter result = null;
        if (adapterView != null) {
            Object ad = adapterView.getAdapter();
            if (ad instanceof CursorAdapter) {
                result = (CursorAdapter) ad;
            } else if (ad instanceof WrapperListAdapter) {
                result = (CursorAdapter) ((WrapperListAdapter)ad).getWrappedAdapter();
            }
        }
        return result;
    }

    /**
     * React to typing in the GO search button by refocusing to EditText. 
     * Continue typing the query.
     */
    View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            // also guard against possible race conditions (late arrival after dismiss)
            if (mSearchable != null) {
                return refocusingKeyListener(v, keyCode, event);
            }
            return false;
        }
    };

    /**
     * React to a click in the GO button by launching a search.
     */
    View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
        public void onClick(View v) {
            // also guard against possible race conditions (late arrival after dismiss)
            if (mSearchable != null) {
                launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
            }
        }
    };
    
    /**
     * React to a click in the voice search button.
     */
    View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
        public void onClick(View v) {
            try {
                if (mSearchable.getVoiceSearchLaunchWebSearch()) {
                    getContext().startActivity(mVoiceWebSearchIntent);
                    dismiss();
                } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
                    Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
                    getContext().startActivity(appSearchIntent);
                    dismiss();
                }
            } catch (ActivityNotFoundException e) {
                // Should not happen, since we check the availability of
                // voice search before showing the button. But just in case...
                Log.w(LOG_TAG, "Could not find voice search activity");
            }
         }
    };
    
    /**
     * Create and return an Intent that can launch the voice search activity, perform a specific
     * voice transcription, and forward the results to the searchable activity.
     * 
     * @param baseIntent The voice app search intent to start from
     * @return A completely-configured intent ready to send to the voice search activity
     */
    private Intent createVoiceAppSearchIntent(Intent baseIntent) {
        // create the necessary intent to set up a search-and-forward operation
        // in the voice search system.   We have to keep the bundle separate,
        // because it becomes immutable once it enters the PendingIntent
        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
        queryIntent.setComponent(mSearchable.mSearchActivity);
        PendingIntent pending = PendingIntent.getActivity(
                getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
        
        // Now set up the bundle that will be inserted into the pending intent
        // when it's time to do the search.  We always build it here (even if empty)
        // because the voice search activity will always need to insert "QUERY" into
        // it anyway.
        Bundle queryExtras = new Bundle();
        if (mAppSearchData != null) {
            queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
        }
        
        // Now build the intent to launch the voice search.  Add all necessary
        // extras to launch the voice recognizer, and then all the necessary extras
        // to forward the results to the searchable activity
        Intent voiceIntent = new Intent(baseIntent);
        
        // Add all of the configuration options supplied by the searchable's metadata
        String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
        String prompt = null;
        String language = null;
        int maxResults = 1;
        Resources resources = mActivityContext.getResources();
        if (mSearchable.getVoiceLanguageModeId() != 0) {
            languageModel = resources.getString(mSearchable.getVoiceLanguageModeId());
        }
        if (mSearchable.getVoicePromptTextId() != 0) {
            prompt = resources.getString(mSearchable.getVoicePromptTextId());
        }
        if (mSearchable.getVoiceLanguageId() != 0) {
            language = resources.getString(mSearchable.getVoiceLanguageId());
        }
        if (mSearchable.getVoiceMaxResults() != 0) {
            maxResults = mSearchable.getVoiceMaxResults();
        }
        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
        
        // Add the values that configure forwarding the results
        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
        
        return voiceIntent;
    }

    /**
     * React to the user typing "enter" or other hardwired keys while typing in the search box.
     * This handles these special keys while the edit box has focus.
     */
    View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                cancel();
                return true;
            }
            // also guard against possible race conditions (late arrival after dismiss)
            if (mSearchable != null && 
                    TextUtils.getTrimmedLength(mSearchTextField.getText()) > 0) {
                if (DBG_LOG_TIMING == 1) {
                    dbgLogTiming("doTextKey()");
                }
                // dispatch "typing in the list" first
                if (mSearchTextField.isPopupShowing() && 
                        mSearchTextField.getListSelection() != ListView.INVALID_POSITION) {
                     return onSuggestionsKey(v, keyCode, event);
                }
                // otherwise, dispatch an "edit view" key
                switch (keyCode) {
                case KeyEvent.KEYCODE_ENTER:
                    if (event.getAction() == KeyEvent.ACTION_UP) {
                        v.cancelLongPress();
                        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);                    
                        return true;
                    }
                    break;
                case KeyEvent.KEYCODE_DPAD_DOWN:                    
                    // capture the EditText state, so we can restore the user entry later
                    mUserQuery = mSearchTextField.getText().toString();
                    mUserQuerySelStart = mSearchTextField.getSelectionStart();
                    mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
                    // pass through - we're just watching here
                    break;
                default:
                    if (event.getAction() == KeyEvent.ACTION_DOWN) {
                        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
                        if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
                            launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
                            return true;
                        }
                    }
                    break;
                }
            }
            return false;
        }
    };

    /**
     * React to the user typing while the suggestions are focused.  First, check for action
     * keys.  If not handled, try refocusing regular characters into the EditText.  In this case,
     * replace the query text (start typing fresh text).
     */
    private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
        boolean handled = false;
        // also guard against possible race conditions (late arrival after dismiss)
        if (mSearchable != null) {
            handled = doSuggestionsKey(v, keyCode, event);
        }
        return handled;
    }
    
    /**
     * Per UI design, we're going to "steer" any typed keystrokes back into the EditText
     * box, even if the user has navigated the focus to the dropdown or to the GO button.
     * 
     * @param v The view into which the keystroke was typed
     * @param keyCode keyCode of entered key
     * @param event Full KeyEvent record of entered key
     */
    private boolean refocusingKeyListener(View v, int keyCode, KeyEvent event) {
        boolean handled = false;

        if (!event.isSystem() && 
                (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
                (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
            // restore focus and give key to EditText ...
            // but don't replace the user's query
            mLeaveJammedQueryOnRefocus = true;
            if (mSearchTextField.requestFocus()) {
                handled = mSearchTextField.dispatchKeyEvent(event);
            }
            mLeaveJammedQueryOnRefocus = false;
        }
        return handled;
    }
    
    /**
     * Update query text based on transitions in and out of suggestions list.
     */
    /*
     * TODO - figure out if this logic is required for the autocomplete text view version

    OnFocusChangeListener mSuggestFocusListener = new OnFocusChangeListener() {
        public void onFocusChange(View v, boolean hasFocus) {
            // also guard against possible race conditions (late arrival after dismiss)
            if (mSearchable == null) {
                return;
            }
            // Update query text based on navigation in to/out of the suggestions list
            if (hasFocus) {
                // Entering the list view - record selection point from user's query
                mUserQuery = mSearchTextField.getText().toString();
                mUserQuerySelStart = mSearchTextField.getSelectionStart();
                mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
                // then update the query to match the entered selection
                jamSuggestionQuery(true, mSuggestionsList, 
                                    mSuggestionsList.getSelectedItemPosition());
            } else {
                // Exiting the list view
                
                if (mSuggestionsList.getSelectedItemPosition() < 0) {
                    // Direct exit - Leave new suggestion in place (do nothing)
                } else {
                    // Navigation exit - restore user's query text
                    if (!mLeaveJammedQueryOnRefocus) {
                        jamSuggestionQuery(false, null, -1);
                    }
                }
            }

        }
    };
    */
    
    /**
     * This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent.  It's an indication that
     * we should close ourselves immediately, in order to allow a higher-priority UI to take over
     * (e.g. phone call received).
     */
    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
                cancel();
            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)
                    || Intent.ACTION_PACKAGE_REMOVED.equals(action)
                    || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
                onPackageListChange();
            }
        }
    };

    @Override
    public void cancel() {
        // We made sure the IME was displayed, so also make sure it is closed
        // when we go away.
        InputMethodManager imm = (InputMethodManager)getContext()
                .getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            imm.hideSoftInputFromWindow(
                    getWindow().getDecorView().getWindowToken(), 0);
        }
        
        super.cancel();
    }
    
    /**
     * Various ways to launch searches
     */

    /**
     * React to the user clicking the "GO" button.  Hide the UI and launch a search.
     *
     * @param actionKey Pass a keycode if the launch was triggered by an action key.  Pass
     * KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
     * @param actionMsg Pass the suggestion-provided message if the launch was triggered by an
     * action key.  Pass null for no actionKey message.
     */
    private void launchQuerySearch(int actionKey, final String actionMsg)  {
        final String query = mSearchTextField.getText().toString();
        final Bundle appData = mAppSearchData;
        final SearchableInfo si = mSearchable;      // cache briefly (dismiss() nulls it)
        dismiss();
        sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, actionKey, actionMsg, si);
    }

    /**
     * React to the user typing an action key while in the suggestions list
     */
    private boolean doSuggestionsKey(View v, int keyCode, KeyEvent event) {
        // Exit early in case of race condition
        if (mSuggestionsAdapter == null) {
            return false;
        }
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (DBG_LOG_TIMING == 1) {
                dbgLogTiming("doSuggestionsKey()");
            }
            
            // First, check for enter or search (both of which we'll treat as a "click")
            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
                int position = mSearchTextField.getListSelection();
                return launchSuggestion(mSuggestionsAdapter, position);
            }
            
            // Next, check for left/right moves, which we use to "return" the user to the edit view
            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
                // give "focus" to text editor, but don't restore the user's original query
                int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 
                        0 : mSearchTextField.length();
                mSearchTextField.setSelection(selPoint);
                mSearchTextField.setListSelection(0);
                mSearchTextField.clearListSelection();
                return true;
            }
            
            // Next, check for an "up and out" move
            if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchTextField.getListSelection()) {
                jamSuggestionQuery(false, null, -1);
                // let ACTV complete the move
                return false;
            }
            
            // Next, check for an "action key"
            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
            if ((actionKey != null) && 
                    ((actionKey.mSuggestActionMsg != null) || 
                     (actionKey.mSuggestActionMsgColumn != null))) {
                //   launch suggestion using action key column
                int position = mSearchTextField.getListSelection();
                if (position >= 0) {
                    Cursor c = mSuggestionsAdapter.getCursor();
                    if (c.moveToPosition(position)) {
                        final String actionMsg = getActionKeyMessage(c, actionKey);
                        if (actionMsg != null && (actionMsg.length() > 0)) {
                            // shut down search bar and launch the activity
                            // cache everything we need because dismiss releases mems
                            setupSuggestionIntent(c, mSearchable);
                            final String query = mSearchTextField.getText().toString();
                            final Bundle appData =  mAppSearchData;
                            SearchableInfo si = mSearchable;
                            String suggestionAction = mSuggestionAction;
                            Uri suggestionData = mSuggestionData;
                            String suggestionQuery = mSuggestionQuery;
                            dismiss();
                            sendLaunchIntent(suggestionAction, suggestionData,
                                    suggestionQuery, appData,
                                             keyCode, actionMsg, si);
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }    

    /**
     * Set or reset the user query to follow the selections in the suggestions
     * 
     * @param jamQuery True means to set the query, false means to reset it to the user's choice
     */
    private void jamSuggestionQuery(boolean jamQuery, AdapterView<?> parent, int position) {
        // quick check against race conditions
        if (mSearchable == null) {
            return;
        }
        
        mSuggestionsAdapter.setNonUserQuery(true);       // disables any suggestions processing
        if (jamQuery) {
            CursorAdapter ca = getSuggestionsAdapter(parent);
            Cursor c = ca.getCursor();
            if (c.moveToPosition(position)) {
                setupSuggestionIntent(c, mSearchable);
                String jamText = null;

                // Simple heuristic for selecting text with which to rewrite the query.
                if (mSuggestionQuery != null) {
                    jamText = mSuggestionQuery;
                } else if (mSearchable.mQueryRewriteFromData && (mSuggestionData != null)) {
                    jamText = mSuggestionData.toString();
                } else if (mSearchable.mQueryRewriteFromText) {
                    try {
                        int column = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1);
                        jamText = c.getString(column);
                    } catch (RuntimeException e) {
                        // no work here, jamText is null
                    }
                }
                if (jamText != null) {
                    mSearchTextField.setText(jamText);
                    /* mSearchTextField.selectAll(); */ // this didn't work anyway in the old UI
                    // TODO this is only needed in the model where we have a selection in the ACTV
                    // and in the dropdown at the same time.
                    mSearchTextField.setSelection(jamText.length());
                }
            }
        } else {
            // reset user query
            mSearchTextField.setText(mUserQuery);
            try {
                mSearchTextField.setSelection(mUserQuerySelStart, mUserQuerySelEnd);
            } catch (IndexOutOfBoundsException e) {
                // In case of error, just select all
                Log.e(LOG_TAG, "Caught IndexOutOfBoundsException while setting selection.  " +
                        "start=" + mUserQuerySelStart + " end=" + mUserQuerySelEnd +
                        " text=\"" + mUserQuery + "\"");
                mSearchTextField.selectAll();
            }
        }
        // TODO because the new query is (not) processed in another thread, we can't just
        // take away this flag (yet).  The better solution here is going to require a new API
        // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
//      mSuggestionsAdapter.setNonUserQuery(false);
    }

    /**
     * Assemble a search intent and send it.
     *
     * @param action The intent to send, typically Intent.ACTION_SEARCH
     * @param data The data for the intent
     * @param query The user text entered (so far)
     * @param appData The app data bundle (if supplied)
     * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will
     * be sent here.  Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
     * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the
     * corresponding tag message will be sent here.  Pass null for no actionKey message.
     * @param si Reference to the current SearchableInfo.  Passed here so it can be used even after
     * we've called dismiss(), which attempts to null mSearchable.
     */
    private void sendLaunchIntent(final String action, final Uri data, final String query,
            final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) {
        Intent launcher = new Intent(action);

        if (query != null) {
            launcher.putExtra(SearchManager.QUERY, query);
        }

        if (data != null) {
            launcher.setData(data);
        }

        if (appData != null) {
            launcher.putExtra(SearchManager.APP_DATA, appData);
        }

        // add launch info (action key, etc.)
        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
            launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
            launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
        }

        // attempt to enforce security requirement (no 3rd-party intents)
        launcher.setComponent(si.mSearchActivity);

        getContext().startActivity(launcher);
    }

    /**
     * Shared code for launching a query from a suggestion.
     * @param ca The cursor adapter containing the suggestions
     * @param position The suggestion we'll be launching from
     * @return true if a successful launch, false if could not (e.g. bad position)
     */
    private boolean launchSuggestion(CursorAdapter ca, int position) {
        Cursor c = ca.getCursor();
        if ((c != null) && c.moveToPosition(position)) {
            setupSuggestionIntent(c, mSearchable);
            
            final Bundle appData =  mAppSearchData;
            SearchableInfo si = mSearchable;
            String suggestionAction = mSuggestionAction;
            Uri suggestionData = mSuggestionData;
            String suggestionQuery = mSuggestionQuery;
            dismiss();
            sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, appData,
                                KeyEvent.KEYCODE_UNKNOWN, null, si);
            return true;
        }
        return false;
    }
    
    /**
     * When a particular suggestion has been selected, perform the various lookups required
     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
     * the suggestion includes a data id.
     * 
     * NOTE:  Return values are in member variables mSuggestionAction & mSuggestionData.
     * 
     * @param c The suggestions cursor, moved to the row of the user's selection
     * @param si The searchable activity's info record
     */
    void setupSuggestionIntent(Cursor c, SearchableInfo si) {
        try {
            // use specific action if supplied, or default action if supplied, or fixed default
            mSuggestionAction = null;
            int mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
            if (mColumn >= 0) {
                final String action = c.getString(mColumn);
                if (action != null) {
                    mSuggestionAction = action;
                }
            }
            if (mSuggestionAction == null) {
                mSuggestionAction = si.getSuggestIntentAction();
            }
            if (mSuggestionAction == null) {
                mSuggestionAction = Intent.ACTION_SEARCH;
            }
            
            // use specific data if supplied, or default data if supplied
            String data = null;
            mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
            if (mColumn >= 0) {
                final String rowData = c.getString(mColumn);
                if (rowData != null) {
                    data = rowData;
                }
            }
            if (data == null) {
                data = si.getSuggestIntentData();
            }
            
            // then, if an ID was provided, append it.
            if (data != null) {
                mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
                if (mColumn >= 0) {
                    final String id = c.getString(mColumn);
                    if (id != null) {
                        data = data + "/" + Uri.encode(id);
                    }
                }
            }
            mSuggestionData = (data == null) ? null : Uri.parse(data);
            
            mSuggestionQuery = null;
            mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
            if (mColumn >= 0) {
                final String query = c.getString(mColumn);
                if (query != null) {
                    mSuggestionQuery = query;
                }
            }
        } catch (RuntimeException e ) {
            int rowNum;
            try {                       // be really paranoid now
                rowNum = c.getPosition();
            } catch (RuntimeException e2 ) {
                rowNum = -1;
            }
            Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum + 
                            " returned exception" + e.toString());
        }
    }
    
    /**
     * For a given suggestion and a given cursor row, get the action message.  If not provided
     * by the specific row/column, also check for a single definition (for the action key).
     * 
     * @param c The cursor providing suggestions
     * @param actionKey The actionkey record being examined
     * 
     * @return Returns a string, or null if no action key message for this suggestion
     */
    private String getActionKeyMessage(Cursor c, final SearchableInfo.ActionKeyInfo actionKey) {
        String result = null;
        // check first in the cursor data, for a suggestion-specific message
        final String column = actionKey.mSuggestActionMsgColumn;
        if (column != null) {
            try {
                int colId = c.getColumnIndexOrThrow(column);
                result = c.getString(colId);
            } catch (RuntimeException e) {
                // OK - result is already null
            }
        }
        // If the cursor didn't give us a message, see if there's a single message defined
        // for the actionkey (for all suggestions)
        if (result == null) {
            result = actionKey.mSuggestActionMsg;
        }
        return result;
    }
        
    /**
     * Local subclass for AutoCompleteTextView
     * 
     * This exists entirely to override the threshold method.  Otherwise we just use the class
     * as-is.
     */
    public static class SearchAutoComplete extends AutoCompleteTextView {

        public SearchAutoComplete(Context context) {
            super(null);
        }
        
        public SearchAutoComplete(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
        
        /**
         * We never allow ACTV to automatically replace the text, since we use "jamSuggestionQuery"
         * to do that.  There's no point in letting ACTV do this here, because in the search UI,
         * as soon as we click a suggestion, we're going to start shutting things down.
         */
        @Override
        public void replaceText(CharSequence text) {
        }
        
        /**
         * We always return true, so that the effective threshold is "zero".  This allows us
         * to provide "null" suggestions such as "just show me some recent entries".
         */
        @Override
        public boolean enoughToFilter() {
            return true;
        }
    }
    
    /**
     * Support for AutoCompleteTextView-based suggestions
     */
    /**
     * This class provides the filtering-based interface to suggestions providers.
     * It is hardwired in a couple of places to support GoogleSearch - for example, it supports
     * two-line suggestions, but it does not support icons.
     */
    private static class SuggestionsAdapter extends SimpleCursorAdapter {
        private final String TAG = "SuggestionsAdapter";
        
        SearchableInfo mSearchable;
        private Resources mProviderResources;
        
        // These private variables are shared by the filter thread and must be protected
        private WeakReference<Cursor> mRecentCursor = new WeakReference<Cursor>(null);
        private boolean mNonUserQuery = false;
        private AutoCompleteTextView mParentView;

        public SuggestionsAdapter(Context context, SearchableInfo searchable,
                AutoCompleteTextView actv) {
            super(context, -1, null, null, null);
            mSearchable = searchable;
            mParentView = actv;
            
            // set up provider resources (gives us icons, etc.)
            Context activityContext = mSearchable.getActivityContext(mContext);
            Context providerContext = mSearchable.getProviderContext(mContext, activityContext);
            mProviderResources = providerContext.getResources();
        }
        
        /**
         * Set this field (temporarily!) to disable suggestions updating.  This allows us
         * to change the string in the text view without changing the suggestions list.
         */
        public void setNonUserQuery(boolean nonUserQuery) {
            synchronized (this) {
                mNonUserQuery = nonUserQuery;
            }
        }
        
        public boolean getNonUserQuery() {
            synchronized (this) {
                return mNonUserQuery;
            }
        }

        /**
         * Use the search suggestions provider to obtain a live cursor.  This will be called
         * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
         * The results will be processed in the UI thread and changeCursor() will be called.
         * 
         * In order to provide the Search Mgr functionality of seeing your query change as you
         * scroll through the list, we have to be able to jam new text into the string without
         * retriggering the suggestions.  We do that here via the "nonUserQuery" flag.  In that
         * case we simply return the existing cursor.
         * 
         * TODO: Dianne suggests that this should simply be promoted into an AutoCompleteTextView
         * behavior (perhaps optionally).
         * 
         * TODO: The "nonuserquery" logic has a race condition because it happens in another thread.
         * This also needs to be fixed.
         */
        @Override
        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
            String query = (constraint == null) ? "" : constraint.toString();
            Cursor c = null;
            synchronized (this) {
                if (mNonUserQuery) {
                    c = mRecentCursor.get();
                    mNonUserQuery = false;
                }
            }
            if (c == null) {
                c = getSuggestions(mSearchable, query);
                synchronized (this) {
                    mRecentCursor = new WeakReference<Cursor>(c);
                }
            }
            return c;
        }
        
        /**
         * Overriding changeCursor() allows us to change not only the cursor, but by sampling
         * the cursor's columns, the actual display characteristics of the list.
         */
        @Override
        public void changeCursor(Cursor c) {
            
            // first, check for various conditions that disqualify this cursor
            if ((c == null) || (c.getCount() == 0)) {
                // no cursor, or cursor with no data
                changeCursorAndColumns(null, null, null);
                if (c != null) {
                    c.close();
                }
                return;
            }

            // check cursor before trying to create list views from it
            int colId = c.getColumnIndex("_id");
            int col1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
            int col2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
            int colIc1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
            int colIc2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);

            boolean minimal = (colId >= 0) && (col1 >= 0);
            boolean hasIcons = (colIc1 >= 0) && (colIc2 >= 0);
            boolean has2Lines = col2 >= 0;

            if (minimal) {
                int layout;
                String[] from;
                int[] to;

                if (hasIcons) {
                    if (has2Lines) {
                        layout = com.android.internal.R.layout.search_dropdown_item_icons_2line;
                        from = TWO_LINE_ICONS_FROM;
                        to = TWO_LINE_ICONS_TO;
                    } else {
                        layout = com.android.internal.R.layout.search_dropdown_item_icons_1line;
                        from = ONE_LINE_ICONS_FROM;
                        to = ONE_LINE_ICONS_TO;
                    }
                } else {
                    if (has2Lines) {
                        layout = com.android.internal.R.layout.search_dropdown_item_2line;
                        from = TWO_LINE_FROM;
                        to = TWO_LINE_TO;
                    } else {
                        layout = com.android.internal.R.layout.search_dropdown_item_1line;
                        from = ONE_LINE_FROM;
                        to = ONE_LINE_TO;
                    }
                }
                // Force the underlying ListView to discard and reload all layouts
                // (Note, this should be optimized for cases where layout/cursor remain same)
                mParentView.resetListAndClearViews();
                // Now actually set up the cursor, columns, and the list view
                changeCursorAndColumns(c, from, to);
                setViewResource(layout);          
            } else {
                // Provide some help for developers instead of just silently discarding
                Log.w(LOG_TAG, "Suggestions cursor discarded due to missing required columns.");
                changeCursorAndColumns(null, null, null);
                c.close();
            }
            if ((colIc1 >= 0) != (colIc2 >= 0)) {
                Log.w(LOG_TAG, "Suggestion icon column(s) discarded, must be 0 or 2 columns.");
            }    
        }
        
        /**
         * Overriding this allows us to write the selected query back into the box.
         * NOTE:  This is a vastly simplified version of SearchDialog.jamQuery() and does
         * not universally support the search API.  But it is sufficient for Google Search.
         */
        @Override
        public CharSequence convertToString(Cursor cursor) {
            CharSequence result = null;
            if (cursor != null) {
                int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
                if (column >= 0) {
                    final String query = cursor.getString(column);
                    if (query != null) {
                        result = query;
                    }
                }
            }
            return result;
        }

        /**
         * Get the query cursor for the search suggestions.
         * 
         * TODO this is functionally identical to the version in SearchDialog.java.  Perhaps it 
         * could be hoisted into SearchableInfo or some other shared spot.
         * 
         * @param query The search text entered (so far)
         * @return Returns a cursor with suggestions, or null if no suggestions 
         */
        private Cursor getSuggestions(final SearchableInfo searchable, final String query) {
            Cursor cursor = null;
            if (searchable.getSuggestAuthority() != null) {
                try {
                    StringBuilder uriStr = new StringBuilder("content://");
                    uriStr.append(searchable.getSuggestAuthority());

                    // if content path provided, insert it now
                    final String contentPath = searchable.getSuggestPath();
                    if (contentPath != null) {
                        uriStr.append('/');
                        uriStr.append(contentPath);
                    }

                    // append standard suggestion query path 
                    uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY);

                    // inject query, either as selection args or inline
                    String[] selArgs = null;
                    if (searchable.getSuggestSelection() != null) {    // use selection if provided
                        selArgs = new String[] {query};
                    } else {
                        uriStr.append('/');                             // no sel, use REST pattern
                        uriStr.append(Uri.encode(query));
                    }

                    // finally, make the query
                    cursor = mContext.getContentResolver().query(
                                                        Uri.parse(uriStr.toString()), null, 
                                                        searchable.getSuggestSelection(), selArgs,
                                                        null);
                } catch (RuntimeException e) {
                    Log.w(TAG, "Search Suggestions query returned exception " + e.toString());
                    cursor = null;
                }
            }
            
            return cursor;
        }

        /**
         * Overriding this allows us to affect the way that an icon is loaded.  Specifically,
         * we can be more controlling about the resource path (and allow icons to come from other
         * packages).
         * 
         * TODO: This is 100% identical to the version in SearchDialog.java
         *
         * @param v ImageView to receive an image
         * @param value the value retrieved from the cursor
         */
        @Override
        public void setViewImage(ImageView v, String value) {
            int resID;
            Drawable img = null;

            try {
                resID = Integer.parseInt(value);
                if (resID != 0) {
                    img = mProviderResources.getDrawable(resID);
                }
            } catch (NumberFormatException nfe) {
                // img = null;
            } catch (NotFoundException e2) {
                // img = null;
            }
            
            // finally, set the image to whatever we've gotten
            v.setImageDrawable(img);
        }
        
        /**
         * This method is overridden purely to provide a bit of protection against
         * flaky content providers.
         * 
         * TODO: This is 100% identical to the version in SearchDialog.java
         * 
         * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
         */
        @Override 
        public View getView(int position, View convertView, ViewGroup parent) {
            try {
                return super.getView(position, convertView, parent);
            } catch (RuntimeException e) {
                Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString());
                // what can I return here?
                View v = newView(mContext, mCursor, parent);
                if (v != null) {
                    TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1);
                    tv.setText(e.toString());
                }
                return v;
            }
        }

    }
    
    /**
     * Implements OnItemClickListener
     */
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // Log.d(LOG_TAG, "onItemClick() position " + position);
        launchSuggestion(mSuggestionsAdapter, position);
    }
    
    /** 
     * Implements OnItemSelectedListener
     */
     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
         // Log.d(LOG_TAG, "onItemSelected() position " + position);
         jamSuggestionQuery(true, parent, position);
     }

     /** 
      * Implements OnItemSelectedListener
      */
     public void onNothingSelected(AdapterView<?> parent) {
         // Log.d(LOG_TAG, "onNothingSelected()");
     }

    /**
     * Debugging Support
     */

    /**
     * For debugging only, sample the millisecond clock and log it.
     * Uses AtomicLong so we can use in multiple threads
     */
    private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
    private void dbgLogTiming(final String caller) {
        long millis = SystemClock.uptimeMillis();
        long oldTime = mLastLogTime.getAndSet(millis);
        long delta = millis - oldTime;
        final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
        Log.d(LOG_TAG,report);
    }
}