Java程序  |  1951行  |  79.95 KB

/*
 * 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 static android.app.SuggestionsAdapter.getColumnString;

import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.provider.Browser;
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.text.util.Regex;
import android.util.AndroidRuntimeException;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;

import java.util.ArrayList;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * System search dialog. This is controlled by the 
 * SearchManagerService and runs in the system process.
 * 
 * @hide
 */
public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {

    // Debugging support
    private static final boolean DBG = false;
    private static final String LOG_TAG = "SearchDialog";
    private static final boolean DBG_LOG_TIMING = false;

    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_STORED_COMPONENT = "sComp";
    private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
    private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev";
    private static final String INSTANCE_KEY_USER_QUERY = "uQry";
    
    // The extra key used in an intent to the speech recognizer for in-app voice search.
    private static final String EXTRA_CALLING_PACKAGE = "calling_package";
    
    // The string used for privateImeOptions to identify to the IME that it should not show
    // a microphone button since one already exists in the search dialog.
    private static final String IME_OPTION_NO_MICROPHONE = "nm";

    private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
    private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;

    // views & widgets
    private TextView mBadgeLabel;
    private ImageView mAppIcon;
    private SearchAutoComplete mSearchAutoComplete;
    private Button mGoButton;
    private ImageButton mVoiceButton;
    private View mSearchPlate;
    private Drawable mWorkingSpinner;

    // interaction with searchable application
    private SearchableInfo mSearchable;
    private ComponentName mLaunchComponent;
    private Bundle mAppSearchData;
    private boolean mGlobalSearchMode;
    private Context mActivityContext;
    
    // Values we store to allow user to toggle between in-app search and global search.
    private ComponentName mStoredComponentName;
    private Bundle mStoredAppSearchData;
    
    // stack of previous searchables, to support the BACK key after
    // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.
    // The top of the stack (= previous searchable) is the last element of the list,
    // since adding and removing is efficient at the end of an ArrayList.
    private ArrayList<ComponentName> mPreviousComponents;

    // For voice searching
    private final Intent mVoiceWebSearchIntent;
    private final Intent mVoiceAppSearchIntent;

    // support for AutoCompleteTextView suggestions display
    private SuggestionsAdapter mSuggestionsAdapter;
    
    // Whether to rewrite queries when selecting suggestions
    private static final boolean REWRITE_QUERIES = true;
    
    // The query entered by the user. This is not changed when selecting a suggestion
    // that modifies the contents of the text field. But if the user then edits
    // the suggestion, the resulting string is saved.
    private String mUserQuery;
    
    // A weak map of drawables we've gotten from other packages, so we don't load them
    // more than once.
    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
            new WeakHashMap<String, Drawable.ConstantState>();

    // Last known IME options value for the search edit text.
    private int mSearchAutoCompleteImeOptions;

    /**
     * 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_GlobalSearchBar);

        // Save voice intent for later queries/launching
        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
        mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);

        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    }

    /**
     * Create the search dialog and any resources that are used for the
     * entire lifetime of the dialog.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Window theWindow = getWindow();
        WindowManager.LayoutParams lp = theWindow.getAttributes();
        lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR;
        lp.width = ViewGroup.LayoutParams.FILL_PARENT;
        // taking up the whole window (even when transparent) is less than ideal,
        // but necessary to show the popup window until the window manager supports
        // having windows anchored by their parent but not clipped by them.
        lp.height = ViewGroup.LayoutParams.FILL_PARENT;
        lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
        lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
        theWindow.setAttributes(lp);

        // Touching outside of the search dialog will dismiss it
        setCanceledOnTouchOutside(true);        
    }

    /**
     * We recreate the dialog view each time it becomes visible so as to limit
     * the scope of any problems with the contained resources.
     */
    private void createContentView() {
        setContentView(com.android.internal.R.layout.search_bar);

        // get the view elements for local access
        SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar);
        searchBar.setSearchDialog(this);

        mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
        mSearchAutoComplete = (SearchAutoComplete)
                findViewById(com.android.internal.R.id.search_src_text);
        mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
        mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
        mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
        mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
        mWorkingSpinner = getContext().getResources().
                getDrawable(com.android.internal.R.drawable.search_spinner);
        mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
                null, null, mWorkingSpinner, null);
        setWorking(false);

        // attach listeners
        mSearchAutoComplete.addTextChangedListener(mTextWatcher);
        mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
        mSearchAutoComplete.setOnItemClickListener(this);
        mSearchAutoComplete.setOnItemSelectedListener(this);
        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
        mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
    }

    /**
     * Set up the search dialog
     * 
     * @return true if search dialog launched, false if not
     */
    public boolean show(String initialQuery, boolean selectInitialQuery,
            ComponentName componentName, Bundle appSearchData, boolean globalSearch) {

        // Reset any stored values from last time dialog was shown.
        mStoredComponentName = null;
        mStoredAppSearchData = null;

        boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData,
                globalSearch);
        if (success) {
            // Display the drop down as soon as possible instead of waiting for the rest of the
            // pending UI stuff to get done, so that things appear faster to the user.
            mSearchAutoComplete.showDropDownAfterLayout();
        }
        return success;
    }

    private boolean isInRealAppSearch() {
        return !mGlobalSearchMode
                && (mPreviousComponents == null || mPreviousComponents.isEmpty());
    }

    /**
     * Called in response to a press of the hard search button in
     * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app
     * search and global search when relevant.
     * 
     * If pressed within an in-app search context, this switches the search dialog out to
     * global search. If pressed within a global search context that was originally an in-app
     * search context, this switches back to the in-app search context. If pressed within a
     * global search context that has no original in-app search context (e.g., global search
     * from Home), this does nothing.
     * 
     * @return false if we wanted to toggle context but could not do so successfully, true
     * in all other cases
     */
    private boolean toggleGlobalSearch() {
        String currentSearchText = mSearchAutoComplete.getText().toString();
        if (!mGlobalSearchMode) {
            mStoredComponentName = mLaunchComponent;
            mStoredAppSearchData = mAppSearchData;
            
            // If this is the browser, we have a special case to not show the icon to the left
            // of the text field, for extra space for url entry (this should be reconciled in
            // Eclair). So special case a second tap of the search button to remove any
            // already-entered text so that we can be sure to show the "Quick Search Box" hint
            // text to still make it clear to the user that we've jumped out to global search.
            //
            // TODO: When the browser icon issue is reconciled in Eclair, remove this special case.
            if (isBrowserSearch()) currentSearchText = "";
            
            return doShow(currentSearchText, false, null, mAppSearchData, true);
        } else {
            if (mStoredComponentName != null) {
                // This means we should toggle *back* to an in-app search context from
                // global search.
                return doShow(currentSearchText, false, mStoredComponentName,
                        mStoredAppSearchData, false);
            } else {
                return true;
            }
        }
    }
    
    /**
     * Does the rest of the work required to show the search dialog. Called by both
     * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and
     * {@link #toggleGlobalSearch()}.
     * 
     * @return true if search dialog showed, false if not
     */
    private boolean doShow(String initialQuery, boolean selectInitialQuery,
            ComponentName componentName, Bundle appSearchData,
            boolean globalSearch) {
        // set up the searchable and show the dialog
        if (!show(componentName, appSearchData, globalSearch)) {
            return false;
        }

        // finally, load the user's initial text (which may trigger suggestions)
        setUserQuery(initialQuery);
        if (selectInitialQuery) {
            mSearchAutoComplete.selectAll();
        }

        return true;
    }

    /**
     * Sets up the search dialog and shows it.
     * 
     * @return <code>true</code> if search dialog launched
     */
    private boolean show(ComponentName componentName, Bundle appSearchData, 
            boolean globalSearch) {
        
        if (DBG) { 
            Log.d(LOG_TAG, "show(" + componentName + ", " 
                    + appSearchData + ", " + globalSearch + ")");
        }
        
        SearchManager searchManager = (SearchManager)
                mContext.getSystemService(Context.SEARCH_SERVICE);
        // Try to get the searchable info for the provided component (or for global search,
        // if globalSearch == true).
        mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
        
        // If we got back nothing, and it wasn't a request for global search, then try again
        // for global search, as we'll try to launch that in lieu of any component-specific search.
        if (!globalSearch && mSearchable == null) {
            globalSearch = true;
            mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
        }

        // If there's not even a searchable info available for global search, then really give up.
        if (mSearchable == null) {
            Log.w(LOG_TAG, "No global search provider.");
            return false;
        }

        mLaunchComponent = componentName;
        mAppSearchData = appSearchData;
        // Using globalSearch here is just an optimization, just calling
        // isDefaultSearchable() should always give the same result.
        mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable);
        mActivityContext = mSearchable.getActivityContext(getContext());

        // show the dialog. this will call onStart().
        if (!isShowing()) {
            // Recreate the search bar view every time the dialog is shown, to get rid
            // of any bad state in the AutoCompleteTextView etc
            createContentView();

            // The Dialog uses a ContextThemeWrapper for the context; use this to change the
            // theme out from underneath us, between the global search theme and the in-app
            // search theme. They are identical except that the global search theme does not
            // dim the background of the window (because global search is full screen so it's
            // not needed and this should save a little bit of time on global search invocation).
            Object context = getContext();
            if (context instanceof ContextThemeWrapper) {
                ContextThemeWrapper wrapper = (ContextThemeWrapper) context;
                if (globalSearch) {
                    wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar);
                } else {
                    wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar);
                }
            }
            show();
        }
        updateUI();
        
        return true;
    }

    /**
     * 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();

        closeSuggestionsAdapter();
        
        // dump extra memory we're hanging on to
        mLaunchComponent = null;
        mAppSearchData = null;
        mSearchable = null;
        mActivityContext = null;
        mUserQuery = null;
        mPreviousComponents = null;
    }

    /**
     * Sets the search dialog to the 'working' state, which shows a working spinner in the
     * right hand size of the text field.
     * 
     * @param working true to show spinner, false to hide spinner
     */
    public void setWorking(boolean working) {
        mWorkingSpinner.setAlpha(working ? 255 : 0);
        mWorkingSpinner.setVisible(working, false);
        mWorkingSpinner.invalidateSelf();
    }
    
    /**
     * Closes and gets rid of the suggestions adapter.
     */
    private void closeSuggestionsAdapter() {
        // remove the adapter from the autocomplete first, to avoid any updates
        // when we drop the cursor
        mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
        // close any leftover cursor
        if (mSuggestionsAdapter != null) {
            mSuggestionsAdapter.close();
        }
        mSuggestionsAdapter = null;
    }
    
    /**
     * Save the minimal set of data necessary to recreate the search
     * 
     * @return A bundle with the state of the dialog, or {@code null} if the search
     *         dialog is not showing.
     */
    @Override
    public Bundle onSaveInstanceState() {
        if (!isShowing()) return null;

        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);
        bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName);
        bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData);
        bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents);
        bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);

        return bundle;
    }

    /**
     * Restore the state of the dialog from a previously saved bundle.
     * 
     * TODO: go through this and make sure that it saves everything that is saved
     *
     * @param savedInstanceState The state of the dialog previously saved by
     *     {@link #onSaveInstanceState()}.
     */
    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState) {
        if (savedInstanceState == null) return;

        ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
        Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
        boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
        ComponentName storedComponentName =
                savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT);
        Bundle storedAppSearchData =
                savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA);
        ArrayList<ComponentName> previousComponents =
                savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS);
        String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);

        // Set stored state
        mStoredComponentName = storedComponentName;
        mStoredAppSearchData = storedAppSearchData;
        mPreviousComponents = previousComponents;

        // show the dialog.
        if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) {
            // for some reason, we couldn't re-instantiate
            return;
        }
    }
    
    /**
     * Called after resources have changed, e.g. after screen rotation or locale change.
     */
    public void onConfigurationChanged() {
        if (isShowing()) {
            // Redraw (resources may have changed)
            updateSearchButton();
            updateSearchAppIcon();
            updateSearchBadge();
            updateQueryHint();
            mSearchAutoComplete.showDropDownAfterLayout();
        } 
    }
    
    /**
     * Update the UI according to the info in the current value of {@link #mSearchable}.
     */
    private void updateUI() {
        if (mSearchable != null) {
            mDecor.setVisibility(View.VISIBLE);
            updateSearchAutoComplete();
            updateSearchButton();
            updateSearchAppIcon();
            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;
                }
            }
            mSearchAutoComplete.setInputType(inputType);
            mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
            mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
            
            // If the search dialog is going to show a voice search button, then don't let
            // the soft keyboard display a microphone button if it would have otherwise.
            if (mSearchable.getVoiceSearchEnabled()) {
                mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
            } else {
                mSearchAutoComplete.setPrivateImeOptions(null);
            }
        }
    }
    
    /**
     * Updates the auto-complete text view.
     */
    private void updateSearchAutoComplete() {
        // close any existing suggestions adapter
        closeSuggestionsAdapter();
        
        mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
        mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
        // we dismiss the entire dialog instead
        mSearchAutoComplete.setDropDownDismissedOnCompletion(false);

        if (!isInRealAppSearch()) {
            mSearchAutoComplete.setDropDownAlwaysVisible(true);  // fill space until results come in
        } else {
            mSearchAutoComplete.setDropDownAlwaysVisible(false);
        }

        mSearchAutoComplete.setForceIgnoreOutsideTouch(true);

        // 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 = new SuggestionsAdapter(getContext(), this, mSearchable, 
                    mOutsideDrawablesCache, mGlobalSearchMode);
            mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
        }
    }

    /**    
     * 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);
    }
    
    private void updateSearchAppIcon() {
        // In Donut, we special-case the case of the browser to hide the app icon as if it were
        // global search, for extra space for url entry.
        //
        // TODO: Remove this special case once the issue has been reconciled in Eclair. 
        if (mGlobalSearchMode || isBrowserSearch()) {
            mAppIcon.setImageResource(0);
            mAppIcon.setVisibility(View.GONE);
            mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
                    mSearchPlate.getPaddingTop(),
                    mSearchPlate.getPaddingRight(),
                    mSearchPlate.getPaddingBottom());
        } else {
            PackageManager pm = getContext().getPackageManager();
            Drawable icon;
            try {
                ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
                icon = pm.getApplicationIcon(info.applicationInfo);
                if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
            } catch (NameNotFoundException e) {
                icon = pm.getDefaultActivityIcon();
                Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
            }
            mAppIcon.setImageDrawable(icon);
            mAppIcon.setVisibility(View.VISIBLE);
            mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
                    mSearchPlate.getPaddingTop(),
                    mSearchPlate.getPaddingRight(),
                    mSearchPlate.getPaddingBottom());
        }
    }

    /**
     * Setup the search "Badge" if requested by mode flags.
     */
    private void updateSearchBadge() {
        // assume both hidden
        int visibility = View.GONE;
        Drawable icon = null;
        CharSequence text = null;
        
        // optionally show one or the other.
        if (mSearchable.useBadgeIcon()) {
            icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
            visibility = View.VISIBLE;
            if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
        } else if (mSearchable.useBadgeLabel()) {
            text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
            visibility = View.VISIBLE;
            if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
        }
        
        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);
                }
            }
            mSearchAutoComplete.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);
    }
    
    /**
     * Hack to determine whether this is the browser, so we can remove the browser icon
     * to the left of the search field, as a special requirement for Donut.
     * 
     * TODO: For Eclair, reconcile this with the rest of the global search UI.
     */
    private boolean isBrowserSearch() {
        return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/");
    }

    /**
     * Listeners of various types
     */

    /**
     * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
     * touch is outside the window. But the window includes space for the drop-down,
     * so we also cancel on taps outside the search bar when the drop-down is not showing.
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // cancel if the drop-down is not showing and the touch event was outside the search plate
        if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
            if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
            cancel();
            return true;
        }
        // Let Dialog handle events outside the window while the pop-up is showing.
        return super.onTouchEvent(event);
    }
    
    private boolean isOutOfBounds(View v, MotionEvent event) {
        final int x = (int) event.getX();
        final int y = (int) event.getY();
        final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
        return (x < -slop) || (y < -slop)
                || (x > (v.getWidth()+slop))
                || (y > (v.getHeight()+slop));
    }
    
    /**
     * 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) {
        if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
        if (mSearchable == null) {
            return false;
        }

        if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() == 0) {
            event.startTracking();
            // Consume search key for later use.
            return true;
        }

        // if it's an action specified by the searchable activity, launch the
        // entered query with the action key
        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
        if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
            launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
            return true;
        }

        return super.onKeyDown(keyCode, event);
    }
    
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (DBG) Log.d(LOG_TAG, "onKeyUp(" + keyCode + "," + event + ")");
        if (mSearchable == null) {
            return false;
        }

        if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking()
                && !event.isCanceled()) {
            // If the search key is pressed, toggle between global and in-app search. If we are
            // currently doing global search and there is no in-app search context to toggle to,
            // just don't do anything.
            return toggleGlobalSearch();
        }

        return super.onKeyUp(keyCode, event);
    }
    
    /**
     * 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) {
                dbgLogTiming("onTextChanged()");
            }
            if (mSearchable == null) {
                return;
            }
            updateWidgetState();
            if (!mSearchAutoComplete.isPerformingCompletion()) {
                // The user changed the query, remember it.
                mUserQuery = s == null ? "" : s.toString();
            }
        }

        public void afterTextChanged(Editable s) {
            if (mSearchable == null) {
                return;
            }
            if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) {
                // The user changed the query, check if it is a URL and if so change the search
                // button in the soft keyboard to the 'Go' button.
                int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION));
                if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) {
                    options = options | EditorInfo.IME_ACTION_GO;
                } else {
                    options = options | EditorInfo.IME_ACTION_SEARCH;
                }
                if (options != mSearchAutoCompleteImeOptions) {
                    mSearchAutoCompleteImeOptions = options;
                    mSearchAutoComplete.setImeOptions(options);
                    // This call is required to update the soft keyboard UI with latest IME flags.
                    mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
                }
            }
        }
    };

    /**
     * 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 = !mSearchAutoComplete.isEmpty();
        mGoButton.setEnabled(enabled);
        mGoButton.setFocusable(enabled);
    }

    /**
     * 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) {
            // guard against possible race conditions
            if (mSearchable == null) {
                return false;
            }
            
            if (!event.isSystem() && 
                    (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
                    (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
                    (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
                    (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
                // restore focus and give key to EditText ...
                if (mSearchAutoComplete.requestFocus()) {
                    return mSearchAutoComplete.dispatchKeyEvent(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) {
            // guard against possible race conditions
            if (mSearchable == null) {
                return;
            }
            launchQuerySearch();
        }
    };
    
    /**
     * React to a click in the voice search button.
     */
    View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
        public void onClick(View v) {
            // guard against possible race conditions
            if (mSearchable == null) {
                return;
            }
            try {
                // First stop the existing search before starting voice search, or else we'll end
                // up showing the search dialog again once we return to the app.
                ((SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE)).
                        stopSearch();
                
                if (mSearchable.getVoiceSearchLaunchWebSearch()) {
                    getContext().startActivity(mVoiceWebSearchIntent);
                } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
                    Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);                    
                    getContext().startActivity(appSearchIntent);
                }
            } 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) {
        ComponentName searchActivity = mSearchable.getSearchActivity();
        
        // 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(searchActivity);
        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);
        voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
                searchActivity == null ? null : searchActivity.toShortString());
        
        // 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;
    }

    /**
     * Corrects http/https typo errors in the given url string, and if the protocol specifier was
     * not present defaults to http.
     * 
     * @param inUrl URL to check and fix
     * @return fixed URL string.
     */
    private String fixUrl(String inUrl) {
        if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
            return inUrl;

        if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
            if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
                inUrl = inUrl.replaceFirst("/", "//");
            } else {
                inUrl = inUrl.replaceFirst(":", "://");
            }
        }

        if (inUrl.indexOf("://") == -1) {
            inUrl = "http://" + inUrl;
        }

        return inUrl;
    }

    /**
     * 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) {
            // guard against possible race conditions
            if (mSearchable == null) {
                return false;
            }

            if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
            if (DBG) { 
                Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event 
                        + "), selection: " + mSearchAutoComplete.getListSelection());
            }
            
            // If a suggestion is selected, handle enter, search key, and action keys 
            // as presses on the selected suggestion
            if (mSearchAutoComplete.isPopupShowing() && 
                    mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
                return onSuggestionsKey(v, keyCode, event);
            }

            // If there is text in the query box, handle enter, and action keys
            // The search key is handled by the dialog's onKeyDown(). 
            if (!mSearchAutoComplete.isEmpty()) {
                if (keyCode == KeyEvent.KEYCODE_ENTER 
                        && event.getAction() == KeyEvent.ACTION_UP) {
                    v.cancelLongPress();

                    // If this is a url entered by the user & we displayed the 'Go' button which
                    // the user clicked, launch the url instead of using it as a search query.
                    if (mSearchable.autoUrlDetect() &&
                        (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
                                == EditorInfo.IME_ACTION_GO) {
                        Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
                        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        launchIntent(intent);
                    } else {
                        // Launch as a regular search.
                        launchQuerySearch();
                    }
                    return true;
                }
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
                    if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
                        launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
                        return true;
                    }
                }
            }
            return false;
        }
    };

    @Override
    public void hide() {
        if (!isShowing()) return;

        // 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.hide();
    }

    /**
     * React to the user typing while in the suggestions list. First, check for action
     * keys. If not handled, try refocusing regular characters into the EditText. 
     */
    private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
        // guard against possible race conditions (late arrival after dismiss)
        if (mSearchable == null) {
            return false;
        }
        if (mSuggestionsAdapter == null) {
            return false;
        }
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (DBG_LOG_TIMING) {
                dbgLogTiming("onSuggestionsKey()");
            }
            
            // 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 = mSearchAutoComplete.getListSelection();
                return launchSuggestion(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, with cursor at the beginning if
                // left key, at end if right key
                // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
                int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 
                        0 : mSearchAutoComplete.length();
                mSearchAutoComplete.setSelection(selPoint);
                mSearchAutoComplete.setListSelection(0);
                mSearchAutoComplete.clearListSelection();
                mSearchAutoComplete.ensureImeVisible();
                
                return true;
            }
            
            // Next, check for an "up and out" move
            if (keyCode == KeyEvent.KEYCODE_DPAD_UP 
                    && 0 == mSearchAutoComplete.getListSelection()) {
                restoreUserQuery();
                // let ACTV complete the move
                return false;
            }
            
            // Next, check for an "action key"
            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
            if ((actionKey != null) && 
                    ((actionKey.getSuggestActionMsg() != null) || 
                     (actionKey.getSuggestActionMsgColumn() != null))) {
                // launch suggestion using action key column
                int position = mSearchAutoComplete.getListSelection();
                if (position != ListView.INVALID_POSITION) {
                    Cursor c = mSuggestionsAdapter.getCursor();
                    if (c.moveToPosition(position)) {
                        final String actionMsg = getActionKeyMessage(c, actionKey);
                        if (actionMsg != null && (actionMsg.length() > 0)) {
                            return launchSuggestion(position, keyCode, actionMsg);
                        }
                    }
                }
            }
        }
        return false;
    }
    
    /**
     * Launch a search for the text in the query text field.
     */
    public void launchQuerySearch()  {
        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
    }

    /**
     * Launch a search for the text in the query text field.
     *
     * @param actionKey The key code of the action key that was pressed,
     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
     * @param actionMsg The message for the action key that was pressed,
     *        or <code>null</code> if none.
     */
    protected void launchQuerySearch(int actionKey, String actionMsg)  {
        String query = mSearchAutoComplete.getText().toString();
        String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH;
        Intent intent = createIntent(action, null, null, query, null,
                actionKey, actionMsg, null);
        // Allow GlobalSearch to log and create shortcut for searches launched by
        // the search button, enter key or an action key.
        if (mGlobalSearchMode) {
            mSuggestionsAdapter.reportSearch(query);
        }
        launchIntent(intent);
    }
    
    /**
     * Launches an intent based on a suggestion.
     * 
     * @param position The index of the suggestion to create the intent from.
     * @return true if a successful launch, false if could not (e.g. bad position).
     */
    protected boolean launchSuggestion(int position) {
        return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
    }
    
    /**
     * Launches an intent based on a suggestion.
     * 
     * @param position The index of the suggestion to create the intent from.
     * @param actionKey The key code of the action key that was pressed,
     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
     * @param actionMsg The message for the action key that was pressed,
     *        or <code>null</code> if none.
     * @return true if a successful launch, false if could not (e.g. bad position).
     */
    protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
        Cursor c = mSuggestionsAdapter.getCursor();
        if ((c != null) && c.moveToPosition(position)) {

            Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);

            // report back about the click
            if (mGlobalSearchMode) {
                // in global search mode, do it via cursor
                mSuggestionsAdapter.callCursorOnClick(c, position, actionKey, actionMsg);
            } else if (intent != null
                    && mPreviousComponents != null
                    && !mPreviousComponents.isEmpty()) {
                // in-app search (and we have pivoted in as told by mPreviousComponents,
                // which is used for keeping track of what we pop back to when we are pivoting into
                // in app search.)
                reportInAppClickToGlobalSearch(c, intent);
            }

            // launch the intent
            launchIntent(intent);

            return true;
        }
        return false;
    }

    /**
     * Report a click from an in app search result back to global search for shortcutting porpoises.
     *
     * @param c The cursor that is pointing to the clicked position.
     * @param intent The intent that will be launched for the click.
     */
    private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) {
        // for in app search, still tell global search via content provider
        Uri uri = getClickReportingUri();
        final ContentValues cv = new ContentValues();
        cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery);
        final ComponentName source = mSearchable.getSearchActivity();
        cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString());

        // grab the intent columns from the intent we created since it has additional
        // logic for falling back on the searchable default
        cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction());
        cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString());
        cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME,
                intent.getComponent().flattenToShortString());

        // ensure the icons will work for global search
        cv.put(SearchManager.SUGGEST_COLUMN_ICON_1,
                        wrapIconForPackage(
                                mSearchable.getSuggestPackage(),
                                getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1)));
        cv.put(SearchManager.SUGGEST_COLUMN_ICON_2,
                        wrapIconForPackage(
                                mSearchable.getSuggestPackage(),
                                getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2)));

        // the rest can be passed through directly
        cv.put(SearchManager.SUGGEST_COLUMN_FORMAT,
                getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT));
        cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
                getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1));
        cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
                getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2));
        cv.put(SearchManager.SUGGEST_COLUMN_QUERY,
                getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY));
        cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
                getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID));
        // note: deliberately omitting background color since it is only for global search
        // "more results" entries
        mContext.getContentResolver().insert(uri, cv);
    }

    /**
     * @return A URI appropriate for reporting a click.
     */
    private Uri getClickReportingUri() {
        Uri.Builder uriBuilder = new Uri.Builder()
                .scheme(ContentResolver.SCHEME_CONTENT)
                .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY);

        uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH);

        return uriBuilder
                .query("")     // TODO: Remove, workaround for a bug in Uri.writeToParcel()
                .fragment("")  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
                .build();
    }

    /**
     * Wraps an icon for a particular package.  If the icon is a resource id, it is converted into
     * an android.resource:// URI.
     *
     * @param packageName The source of the icon
     * @param icon The icon retrieved from a suggestion column
     * @return An icon string appropriate for the package.
     */
    private String wrapIconForPackage(String packageName, String icon) {
        if (icon == null || icon.length() == 0 || "0".equals(icon)) {
            // SearchManager specifies that null or zero can be returned to indicate
            // no icon. We also allow empty string.
            return null;
        } else if (!Character.isDigit(icon.charAt(0))){
            return icon;
        } else {
            return new Uri.Builder()
                    .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
                    .authority(packageName)
                    .encodedPath(icon)
                    .toString();
        }
    }

    /**
     * Launches an intent, including any special intent handling.  Doesn't dismiss the dialog
     * since that will be handled in {@link SearchDialogWrapper#performActivityResuming}
     */
    private void launchIntent(Intent intent) {
        if (intent == null) {
            return;
        }
        if (handleSpecialIntent(intent)){
            return;
        }
        Log.d(LOG_TAG, "launching " + intent);
        try {
            // in global search mode, we send the activity straight to the original suggestion
            // source. this is because GlobalSearch may not have permission to launch the
            // intent, and to avoid the extra step of going through GlobalSearch.
            if (mGlobalSearchMode) {
                launchGlobalSearchIntent(intent);
                if (mStoredComponentName != null) {
                    // If we're embedded in an application, dismiss the dialog.
                    // This ensures that if the intent is handled by the current
                    // activity, it's not obscured by the dialog.
                    dismiss();
                }
            } else {
                // If the intent was created from a suggestion, it will always have an explicit
                // component here.
                Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI());
                getContext().startActivity(intent);
                // If the search switches to a different activity,
                // SearchDialogWrapper#performActivityResuming
                // will handle hiding the dialog when the next activity starts, but for
                // real in-app search, we still need to dismiss the dialog.
                if (isInRealAppSearch()) {
                    dismiss();
                }
            }
        } catch (RuntimeException ex) {
            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
        }
    }

    private void launchGlobalSearchIntent(Intent intent) {
        final String packageName;
        // GlobalSearch puts the original source of the suggestion in the
        // 'component name' column. If set, we send the intent to that activity.
        // We trust GlobalSearch to always set this to the suggestion source.
        String intentComponent = intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY);
        if (intentComponent != null) {
            ComponentName componentName = ComponentName.unflattenFromString(intentComponent);
            intent.setComponent(componentName);
            intent.removeExtra(SearchManager.COMPONENT_NAME_KEY);
            // Launch the intent as the suggestion source.
            // This prevents sources from using the search dialog to launch
            // intents that they don't have permission for themselves.
            packageName = componentName.getPackageName();
        } else {
            // If there is no component in the suggestion, it must be a built-in suggestion
            // from GlobalSearch (e.g. "Search the web for") or the intent
            // launched when pressing the search/go button in the search dialog.
            // Launch the intent with the permissions of GlobalSearch.
            packageName = mSearchable.getSearchActivity().getPackageName();
        }

        // Launch all global search suggestions as new tasks, since they don't relate
        // to the current task.
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        setBrowserApplicationId(intent);

        startActivityInPackage(intent, packageName);
    }

    /**
     * If the intent is to open an HTTP or HTTPS URL, we set
     * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that
     * has been opened by us for the same URL will be reused.
     */
    private void setBrowserApplicationId(Intent intent) {
        Uri data = intent.getData();
        if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) {
            String scheme = data.getScheme();
            if (scheme != null && scheme.startsWith("http")) {
                intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString());
            }
        }
    }

    /**
     * Starts an activity as if it had been started by the given package.
     *
     * @param intent The description of the activity to start.
     * @param packageName
     * @throws ActivityNotFoundException If the intent could not be resolved to
     *         and existing activity.
     * @throws SecurityException If the package does not have permission to start
     *         start the activity.
     * @throws AndroidRuntimeException If some other error occurs.
     */
    private void startActivityInPackage(Intent intent, String packageName) {
        try {
            int uid = ActivityThread.getPackageManager().getPackageUid(packageName);
            if (uid < 0) {
                throw new AndroidRuntimeException("Package UID not found " + packageName);
            }
            String resolvedType = intent.resolveTypeIfNeeded(getContext().getContentResolver());
            IBinder resultTo = null;
            String resultWho = null;
            int requestCode = -1;
            boolean onlyIfNeeded = false;
            Log.i(LOG_TAG, "Starting (uid " + uid + ", " + packageName + ") " + intent.toURI());
            int result = ActivityManagerNative.getDefault().startActivityInPackage(
                    uid, intent, resolvedType, resultTo, resultWho, requestCode, onlyIfNeeded);
            checkStartActivityResult(result, intent);
        } catch (RemoteException ex) {
            throw new AndroidRuntimeException(ex);
        }
    }

    // Stolen from Instrumentation.checkStartActivityResult()
    private static void checkStartActivityResult(int res, Intent intent) {
        if (res >= IActivityManager.START_SUCCESS) {
            return;
        }
        switch (res) {
            case IActivityManager.START_INTENT_NOT_RESOLVED:
            case IActivityManager.START_CLASS_NOT_FOUND:
                if (intent.getComponent() != null)
                    throw new ActivityNotFoundException(
                            "Unable to find explicit activity class "
                            + intent.getComponent().toShortString()
                            + "; have you declared this activity in your AndroidManifest.xml?");
                throw new ActivityNotFoundException(
                        "No Activity found to handle " + intent);
            case IActivityManager.START_PERMISSION_DENIED:
                throw new SecurityException("Not allowed to start activity "
                        + intent);
            case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
                throw new AndroidRuntimeException(
                        "FORWARD_RESULT_FLAG used while also requesting a result");
            default:
                throw new AndroidRuntimeException("Unknown error code "
                        + res + " when starting " + intent);
        }
    }

    /**
     * Handles the special intent actions declared in {@link SearchManager}.
     * 
     * @return <code>true</code> if the intent was handled.
     */
    private boolean handleSpecialIntent(Intent intent) {
        String action = intent.getAction();
        if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
            handleChangeSourceIntent(intent);
            return true;
        }
        return false;
    }
    
    /**
     * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}.
     */
    private void handleChangeSourceIntent(Intent intent) {
        Uri dataUri = intent.getData();
        if (dataUri == null) {
            Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
            return;
        }
        ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
        if (componentName == null) {
            Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
            return;
        }
        if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);

        pushPreviousComponent(mLaunchComponent);
        if (!show(componentName, mAppSearchData, false)) {
            Log.w(LOG_TAG, "Failed to switch to source " + componentName);
            popPreviousComponent();
            return;
        }

        String query = intent.getStringExtra(SearchManager.QUERY);
        setUserQuery(query);
        mSearchAutoComplete.showDropDown();
    }

    /**
     * Sets the list item selection in the AutoCompleteTextView's ListView.
     */
    public void setListSelection(int index) {
        mSearchAutoComplete.setListSelection(index);
    }

    /**
     * Checks if there are any previous searchable components in the history stack.
     */
    private boolean hasPreviousComponent() {
        return mPreviousComponents != null && !mPreviousComponents.isEmpty();
    }

    /**
     * Saves the previous component that was searched, so that we can go
     * back to it.
     */
    private void pushPreviousComponent(ComponentName componentName) {
        if (mPreviousComponents == null) {
            mPreviousComponents = new ArrayList<ComponentName>();
        }
        mPreviousComponents.add(componentName);
    }
    
    /**
     * Pops the previous component off the stack and returns it.
     * 
     * @return The component name, or <code>null</code> if there was
     *         no previous component.
     */
    private ComponentName popPreviousComponent() {
        if (!hasPreviousComponent()) {
            return null;
        }
        return mPreviousComponents.remove(mPreviousComponents.size() - 1);
    }
    
    /**
     * Goes back to the previous component that was searched, if any.
     * 
     * @return <code>true</code> if there was a previous component that we could go back to.
     */
    private boolean backToPreviousComponent() {
        ComponentName previous = popPreviousComponent();
        if (previous == null) {
            return false;
        }

        if (!show(previous, mAppSearchData, false)) {
            Log.w(LOG_TAG, "Failed to switch to source " + previous);
            return false;
        }

        // must touch text to trigger suggestions
        // TODO: should this be the text as it was when the user left
        // the source that we are now going back to?
        String query = mSearchAutoComplete.getText().toString();
        setUserQuery(query);
        return true;
    }
    
    /**
     * 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.
     * 
     * @param c The suggestions cursor, moved to the row of the user's selection
     * @param actionKey The key code of the action key that was pressed,
     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
     * @param actionMsg The message for the action key that was pressed,
     *        or <code>null</code> if none.
     * @return An intent for the suggestion at the cursor's position.
     */
    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
        try {
            // use specific action if supplied, or default action if supplied, or fixed default
            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);

            // some items are display only, or have effect via the cursor respond click reporting.
            if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
                return null;
            }

            if (action == null) {
                action = mSearchable.getSuggestIntentAction();
            }
            if (action == null) {
                action = Intent.ACTION_SEARCH;
            }
            
            // use specific data if supplied, or default data if supplied
            String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
            if (data == null) {
                data = mSearchable.getSuggestIntentData();
            }
            // then, if an ID was provided, append it.
            if (data != null) {
                String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
                if (id != null) {
                    data = data + "/" + Uri.encode(id);
                }
            }
            Uri dataUri = (data == null) ? null : Uri.parse(data);

            String componentName = getColumnString(
                    c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);

            String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
            String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
            String mode = mGlobalSearchMode ? SearchManager.MODE_GLOBAL_SEARCH_SUGGESTION : null;

            return createIntent(action, dataUri, extraData, query, componentName, actionKey,
                    actionMsg, mode);
        } 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());
            return null;
        }
    }
    
    /**
     * Constructs an intent from the given information and the search dialog state.
     * 
     * @param action Intent action.
     * @param data Intent data, or <code>null</code>.
     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
     * @param query Intent query, or <code>null</code>.
     * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
     * @param actionKey The key code of the action key that was pressed,
     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
     * @param actionMsg The message for the action key that was pressed,
     *        or <code>null</code> if none.
     * @param mode The search mode, one of the acceptable values for
     *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
     * @return The intent.
     */
    private Intent createIntent(String action, Uri data, String extraData, String query,
            String componentName, int actionKey, String actionMsg, String mode) {
        // Now build the Intent
        Intent intent = new Intent(action);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        // We need CLEAR_TOP to avoid reusing an old task that has other activities
        // on top of the one we want. We don't want to do this in in-app search though,
        // as it can be destructive to the activity stack.
        if (mGlobalSearchMode) {
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        }
        if (data != null) {
            intent.setData(data);
        }
        intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
        if (query != null) {
            intent.putExtra(SearchManager.QUERY, query);
        }
        if (extraData != null) {
            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
        }
        if (componentName != null) {
            intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName);
        }
        if (mAppSearchData != null) {
            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
        }
        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
        }
        if (mode != null) {
            intent.putExtra(SearchManager.SEARCH_MODE, mode);
        }
        // Only allow 3rd-party intents from GlobalSearch
        if (!mGlobalSearchMode) {
            intent.setComponent(mSearchable.getSearchActivity());
        }
        return intent;
    }
    
    /**
     * 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 static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
        String result = null;
        // check first in the cursor data, for a suggestion-specific message
        final String column = actionKey.getSuggestActionMsgColumn();
        if (column != null) {
            result = SuggestionsAdapter.getColumnString(c, column);
        }
        // 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.getSuggestActionMsg();
        }
        return result;
    }

    /**
     * The root element in the search bar layout. This is a custom view just to override
     * the handling of the back button.
     */
    public static class SearchBar extends LinearLayout {

        private SearchDialog mSearchDialog;

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

        public SearchBar(Context context) {
            super(context);
        }

        public void setSearchDialog(SearchDialog searchDialog) {
            mSearchDialog = searchDialog;
        }

        /**
         * Overrides the handling of the back key to move back to the previous sources or dismiss
         * the search dialog, instead of dismissing the input method.
         */
        @Override
        public boolean dispatchKeyEventPreIme(KeyEvent event) {
            if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")");
            if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                KeyEvent.DispatcherState state = getKeyDispatcherState();
                if (state != null) {
                    if (event.getAction() == KeyEvent.ACTION_DOWN
                            && event.getRepeatCount() == 0) {
                        state.startTracking(event, this);
                        return true;
                    } else if (event.getAction() == KeyEvent.ACTION_UP
                            && !event.isCanceled() && state.isTracking(event)) {
                        mSearchDialog.onBackPressed();
                        return true;
                    }
                }
            }
            return super.dispatchKeyEventPreIme(event);
        }
    }

    /**
     * Local subclass for AutoCompleteTextView.
     */
    public static class SearchAutoComplete extends AutoCompleteTextView {

        private int mThreshold;

        public SearchAutoComplete(Context context) {
            super(context);
            mThreshold = getThreshold();
        }
        
        public SearchAutoComplete(Context context, AttributeSet attrs) {
            super(context, attrs);
            mThreshold = getThreshold();
        }

        public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            mThreshold = getThreshold();
        }

        @Override
        public void setThreshold(int threshold) {
            super.setThreshold(threshold);
            mThreshold = threshold;
        }

        /**
         * Returns true if the text field is empty, or contains only whitespace.
         */
        private boolean isEmpty() {
            return TextUtils.getTrimmedLength(getText()) == 0;
        }

        /**
         * We override this method to avoid replacing the query box text
         * when a suggestion is clicked.
         */
        @Override
        protected void replaceText(CharSequence text) {
        }
        
        /**
         * We override this method to avoid an extra onItemClick being called on the
         * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)}
         * when an item is clicked with the trackball.
         */
        @Override
        public void performCompletion() {
        }

        /**
         * We override this method to be sure and show the soft keyboard if appropriate when
         * the TextView has focus.
         */
        @Override
        public void onWindowFocusChanged(boolean hasWindowFocus) {
            super.onWindowFocusChanged(hasWindowFocus);

            if (hasWindowFocus) {
                InputMethodManager inputManager = (InputMethodManager)
                        getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                inputManager.showSoftInput(this, 0);
            }
        }
                
        /**
         * We override this method so that we can allow a threshold of zero, which ACTV does not.
         */
        @Override
        public boolean enoughToFilter() {
            return mThreshold <= 0 || super.enoughToFilter();
        }

    }

    @Override
    public void onBackPressed() {
        // If the input method is covering the search dialog completely,
        // e.g. in landscape mode with no hard keyboard, dismiss just the input method
        InputMethodManager imm = (InputMethodManager)getContext()
                .getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null && imm.isFullscreenMode() &&
                imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) {
            return;
        }
        // Otherwise, go back to any previous source (e.g. back to QSB when
        // pivoted into a source.
        if (!backToPreviousComponent()) {
            // If no previous source, close search dialog
            cancel();
        }
    }

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

    /** 
     * Implements OnItemSelectedListener
     */
     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
         if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
         // A suggestion has been selected, rewrite the query if possible,
         // otherwise the restore the original query.
         if (REWRITE_QUERIES) {
             rewriteQueryFromSuggestion(position);
         }
     }

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

     private void rewriteQueryFromSuggestion(int position) {
         Cursor c = mSuggestionsAdapter.getCursor();
         if (c == null) {
             return;
         }
         if (c.moveToPosition(position)) {
             // Get the new query from the suggestion.
             CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
             if (newQuery != null) {
                 // The suggestion rewrites the query.
                 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
                 // Update the text field, without getting new suggestions.
                 setQuery(newQuery);
             } else {
                 // The suggestion does not rewrite the query, restore the user's query.
                 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
                 restoreUserQuery();
             }
         } else {
             // We got a bad position, restore the user's query.
             Log.w(LOG_TAG, "Bad suggestion position: " + position);
             restoreUserQuery();
         }
     }
     
     /** 
      * Restores the query entered by the user if needed.
      */
     private void restoreUserQuery() {
         if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
         setQuery(mUserQuery);
     }
     
     /**
      * Sets the text in the query box, without updating the suggestions.
      */
     private void setQuery(CharSequence query) {
         mSearchAutoComplete.setText(query, false);
         if (query != null) {
             mSearchAutoComplete.setSelection(query.length());
         }
     }
     
     /**
      * Sets the text in the query box, updating the suggestions.
      */
     private void setUserQuery(String query) {
         if (query == null) {
             query = "";
         }
         mUserQuery = query;
         mSearchAutoComplete.setText(query);
         mSearchAutoComplete.setSelection(query.length());
     }

    /**
     * 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);
    }
}