Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.app;
     18 
     19 
     20 import static android.app.SuggestionsAdapter.getColumnString;
     21 
     22 import java.util.WeakHashMap;
     23 import java.util.concurrent.atomic.AtomicLong;
     24 
     25 import android.content.ActivityNotFoundException;
     26 import android.content.BroadcastReceiver;
     27 import android.content.ComponentName;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.IntentFilter;
     31 import android.content.pm.ActivityInfo;
     32 import android.content.pm.PackageManager;
     33 import android.content.pm.ResolveInfo;
     34 import android.content.pm.PackageManager.NameNotFoundException;
     35 import android.content.res.Configuration;
     36 import android.content.res.Resources;
     37 import android.database.Cursor;
     38 import android.graphics.drawable.Drawable;
     39 import android.net.Uri;
     40 import android.os.Bundle;
     41 import android.os.SystemClock;
     42 import android.provider.Browser;
     43 import android.speech.RecognizerIntent;
     44 import android.text.Editable;
     45 import android.text.InputType;
     46 import android.text.TextUtils;
     47 import android.text.TextWatcher;
     48 import android.util.AttributeSet;
     49 import android.util.Log;
     50 import android.view.Gravity;
     51 import android.view.KeyEvent;
     52 import android.view.MotionEvent;
     53 import android.view.View;
     54 import android.view.ViewConfiguration;
     55 import android.view.ViewGroup;
     56 import android.view.Window;
     57 import android.view.WindowManager;
     58 import android.view.inputmethod.EditorInfo;
     59 import android.view.inputmethod.InputMethodManager;
     60 import android.widget.AdapterView;
     61 import android.widget.AutoCompleteTextView;
     62 import android.widget.Button;
     63 import android.widget.ImageButton;
     64 import android.widget.ImageView;
     65 import android.widget.LinearLayout;
     66 import android.widget.ListView;
     67 import android.widget.TextView;
     68 import android.widget.AdapterView.OnItemClickListener;
     69 import android.widget.AdapterView.OnItemSelectedListener;
     70 
     71 /**
     72  * Search dialog. This is controlled by the
     73  * SearchManager and runs in the current foreground process.
     74  *
     75  * @hide
     76  */
     77 public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
     78 
     79     // Debugging support
     80     private static final boolean DBG = false;
     81     private static final String LOG_TAG = "SearchDialog";
     82     private static final boolean DBG_LOG_TIMING = false;
     83 
     84     private static final String INSTANCE_KEY_COMPONENT = "comp";
     85     private static final String INSTANCE_KEY_APPDATA = "data";
     86     private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
     87     private static final String INSTANCE_KEY_USER_QUERY = "uQry";
     88 
     89     // The string used for privateImeOptions to identify to the IME that it should not show
     90     // a microphone button since one already exists in the search dialog.
     91     private static final String IME_OPTION_NO_MICROPHONE = "nm";
     92 
     93     private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
     94     private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
     95 
     96     // views & widgets
     97     private TextView mBadgeLabel;
     98     private ImageView mAppIcon;
     99     private SearchAutoComplete mSearchAutoComplete;
    100     private Button mGoButton;
    101     private ImageButton mVoiceButton;
    102     private View mSearchPlate;
    103     private Drawable mWorkingSpinner;
    104 
    105     // interaction with searchable application
    106     private SearchableInfo mSearchable;
    107     private ComponentName mLaunchComponent;
    108     private Bundle mAppSearchData;
    109     private Context mActivityContext;
    110     private SearchManager mSearchManager;
    111 
    112     // For voice searching
    113     private final Intent mVoiceWebSearchIntent;
    114     private final Intent mVoiceAppSearchIntent;
    115 
    116     // support for AutoCompleteTextView suggestions display
    117     private SuggestionsAdapter mSuggestionsAdapter;
    118 
    119     // Whether to rewrite queries when selecting suggestions
    120     private static final boolean REWRITE_QUERIES = true;
    121 
    122     // The query entered by the user. This is not changed when selecting a suggestion
    123     // that modifies the contents of the text field. But if the user then edits
    124     // the suggestion, the resulting string is saved.
    125     private String mUserQuery;
    126     // The query passed in when opening the SearchDialog.  Used in the browser
    127     // case to determine whether the user has edited the query.
    128     private String mInitialQuery;
    129 
    130     // A weak map of drawables we've gotten from other packages, so we don't load them
    131     // more than once.
    132     private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
    133             new WeakHashMap<String, Drawable.ConstantState>();
    134 
    135     // Last known IME options value for the search edit text.
    136     private int mSearchAutoCompleteImeOptions;
    137 
    138     private BroadcastReceiver mConfChangeListener = new BroadcastReceiver() {
    139         @Override
    140         public void onReceive(Context context, Intent intent) {
    141             if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
    142                 onConfigurationChanged();
    143             }
    144         }
    145     };
    146 
    147     /**
    148      * Constructor - fires it up and makes it look like the search UI.
    149      *
    150      * @param context Application Context we can use for system acess
    151      */
    152     public SearchDialog(Context context, SearchManager searchManager) {
    153         super(context, com.android.internal.R.style.Theme_SearchBar);
    154 
    155         // Save voice intent for later queries/launching
    156         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
    157         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    158         mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
    159                 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
    160 
    161         mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    162         mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    163         mSearchManager = searchManager;
    164     }
    165 
    166     /**
    167      * Create the search dialog and any resources that are used for the
    168      * entire lifetime of the dialog.
    169      */
    170     @Override
    171     protected void onCreate(Bundle savedInstanceState) {
    172         super.onCreate(savedInstanceState);
    173 
    174         Window theWindow = getWindow();
    175         WindowManager.LayoutParams lp = theWindow.getAttributes();
    176         lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
    177         // taking up the whole window (even when transparent) is less than ideal,
    178         // but necessary to show the popup window until the window manager supports
    179         // having windows anchored by their parent but not clipped by them.
    180         lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
    181         lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
    182         lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
    183         theWindow.setAttributes(lp);
    184 
    185         // Touching outside of the search dialog will dismiss it
    186         setCanceledOnTouchOutside(true);
    187     }
    188 
    189     /**
    190      * We recreate the dialog view each time it becomes visible so as to limit
    191      * the scope of any problems with the contained resources.
    192      */
    193     private void createContentView() {
    194         setContentView(com.android.internal.R.layout.search_bar);
    195 
    196         // get the view elements for local access
    197         SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar);
    198         searchBar.setSearchDialog(this);
    199 
    200         mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
    201         mSearchAutoComplete = (SearchAutoComplete)
    202                 findViewById(com.android.internal.R.id.search_src_text);
    203         mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
    204         mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
    205         mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
    206         mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
    207         mWorkingSpinner = getContext().getResources().
    208                 getDrawable(com.android.internal.R.drawable.search_spinner);
    209         mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
    210                 null, null, mWorkingSpinner, null);
    211         setWorking(false);
    212 
    213         // attach listeners
    214         mSearchAutoComplete.addTextChangedListener(mTextWatcher);
    215         mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
    216         mSearchAutoComplete.setOnItemClickListener(this);
    217         mSearchAutoComplete.setOnItemSelectedListener(this);
    218         mGoButton.setOnClickListener(mGoButtonClickListener);
    219         mGoButton.setOnKeyListener(mButtonsKeyListener);
    220         mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
    221         mVoiceButton.setOnKeyListener(mButtonsKeyListener);
    222 
    223         // pre-hide all the extraneous elements
    224         mBadgeLabel.setVisibility(View.GONE);
    225 
    226         // Additional adjustments to make Dialog work for Search
    227         mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
    228     }
    229 
    230     /**
    231      * Set up the search dialog
    232      *
    233      * @return true if search dialog launched, false if not
    234      */
    235     public boolean show(String initialQuery, boolean selectInitialQuery,
    236             ComponentName componentName, Bundle appSearchData) {
    237         boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData);
    238         if (success) {
    239             // Display the drop down as soon as possible instead of waiting for the rest of the
    240             // pending UI stuff to get done, so that things appear faster to the user.
    241             mSearchAutoComplete.showDropDownAfterLayout();
    242         }
    243         return success;
    244     }
    245 
    246     /**
    247      * Does the rest of the work required to show the search dialog. Called by
    248      * {@link #show(String, boolean, ComponentName, Bundle)} and
    249      *
    250      * @return true if search dialog showed, false if not
    251      */
    252     private boolean doShow(String initialQuery, boolean selectInitialQuery,
    253             ComponentName componentName, Bundle appSearchData) {
    254         // set up the searchable and show the dialog
    255         if (!show(componentName, appSearchData)) {
    256             return false;
    257         }
    258 
    259         mInitialQuery = initialQuery == null ? "" : initialQuery;
    260         // finally, load the user's initial text (which may trigger suggestions)
    261         setUserQuery(initialQuery);
    262         if (selectInitialQuery) {
    263             mSearchAutoComplete.selectAll();
    264         }
    265 
    266         return true;
    267     }
    268 
    269     /**
    270      * Sets up the search dialog and shows it.
    271      *
    272      * @return <code>true</code> if search dialog launched
    273      */
    274     private boolean show(ComponentName componentName, Bundle appSearchData) {
    275 
    276         if (DBG) {
    277             Log.d(LOG_TAG, "show(" + componentName + ", "
    278                     + appSearchData + ")");
    279         }
    280 
    281         SearchManager searchManager = (SearchManager)
    282                 mContext.getSystemService(Context.SEARCH_SERVICE);
    283         // Try to get the searchable info for the provided component.
    284         mSearchable = searchManager.getSearchableInfo(componentName);
    285 
    286         if (mSearchable == null) {
    287             return false;
    288         }
    289 
    290         mLaunchComponent = componentName;
    291         mAppSearchData = appSearchData;
    292         mActivityContext = mSearchable.getActivityContext(getContext());
    293 
    294         // show the dialog. this will call onStart().
    295         if (!isShowing()) {
    296             // Recreate the search bar view every time the dialog is shown, to get rid
    297             // of any bad state in the AutoCompleteTextView etc
    298             createContentView();
    299 
    300             show();
    301         }
    302         updateUI();
    303 
    304         return true;
    305     }
    306 
    307     @Override
    308     public void onStart() {
    309         super.onStart();
    310 
    311         // Register a listener for configuration change events.
    312         IntentFilter filter = new IntentFilter();
    313         filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
    314         getContext().registerReceiver(mConfChangeListener, filter);
    315     }
    316 
    317     /**
    318      * The search dialog is being dismissed, so handle all of the local shutdown operations.
    319      *
    320      * This function is designed to be idempotent so that dismiss() can be safely called at any time
    321      * (even if already closed) and more likely to really dump any memory.  No leaks!
    322      */
    323     @Override
    324     public void onStop() {
    325         super.onStop();
    326 
    327         getContext().unregisterReceiver(mConfChangeListener);
    328 
    329         closeSuggestionsAdapter();
    330 
    331         // dump extra memory we're hanging on to
    332         mLaunchComponent = null;
    333         mAppSearchData = null;
    334         mSearchable = null;
    335         mUserQuery = null;
    336         mInitialQuery = null;
    337     }
    338 
    339     /**
    340      * Sets the search dialog to the 'working' state, which shows a working spinner in the
    341      * right hand size of the text field.
    342      *
    343      * @param working true to show spinner, false to hide spinner
    344      */
    345     public void setWorking(boolean working) {
    346         mWorkingSpinner.setAlpha(working ? 255 : 0);
    347         mWorkingSpinner.setVisible(working, false);
    348         mWorkingSpinner.invalidateSelf();
    349     }
    350 
    351     /**
    352      * Closes and gets rid of the suggestions adapter.
    353      */
    354     private void closeSuggestionsAdapter() {
    355         // remove the adapter from the autocomplete first, to avoid any updates
    356         // when we drop the cursor
    357         mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
    358         // close any leftover cursor
    359         if (mSuggestionsAdapter != null) {
    360             mSuggestionsAdapter.close();
    361         }
    362         mSuggestionsAdapter = null;
    363     }
    364 
    365     /**
    366      * Save the minimal set of data necessary to recreate the search
    367      *
    368      * @return A bundle with the state of the dialog, or {@code null} if the search
    369      *         dialog is not showing.
    370      */
    371     @Override
    372     public Bundle onSaveInstanceState() {
    373         if (!isShowing()) return null;
    374 
    375         Bundle bundle = new Bundle();
    376 
    377         // setup info so I can recreate this particular search
    378         bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
    379         bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
    380         bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
    381 
    382         return bundle;
    383     }
    384 
    385     /**
    386      * Restore the state of the dialog from a previously saved bundle.
    387      *
    388      * TODO: go through this and make sure that it saves everything that is saved
    389      *
    390      * @param savedInstanceState The state of the dialog previously saved by
    391      *     {@link #onSaveInstanceState()}.
    392      */
    393     @Override
    394     public void onRestoreInstanceState(Bundle savedInstanceState) {
    395         if (savedInstanceState == null) return;
    396 
    397         ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
    398         Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
    399         String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
    400 
    401         // show the dialog.
    402         if (!doShow(userQuery, false, launchComponent, appSearchData)) {
    403             // for some reason, we couldn't re-instantiate
    404             return;
    405         }
    406     }
    407 
    408     /**
    409      * Called after resources have changed, e.g. after screen rotation or locale change.
    410      */
    411     public void onConfigurationChanged() {
    412         if (mSearchable != null && isShowing()) {
    413             // Redraw (resources may have changed)
    414             updateSearchButton();
    415             updateSearchAppIcon();
    416             updateSearchBadge();
    417             updateQueryHint();
    418             if (isLandscapeMode(getContext())) {
    419                 mSearchAutoComplete.ensureImeVisible(true);
    420             }
    421             mSearchAutoComplete.showDropDownAfterLayout();
    422         }
    423     }
    424 
    425     static boolean isLandscapeMode(Context context) {
    426         return context.getResources().getConfiguration().orientation
    427                 == Configuration.ORIENTATION_LANDSCAPE;
    428     }
    429 
    430     /**
    431      * Update the UI according to the info in the current value of {@link #mSearchable}.
    432      */
    433     private void updateUI() {
    434         if (mSearchable != null) {
    435             mDecor.setVisibility(View.VISIBLE);
    436             updateSearchAutoComplete();
    437             updateSearchButton();
    438             updateSearchAppIcon();
    439             updateSearchBadge();
    440             updateQueryHint();
    441             updateVoiceButton(TextUtils.isEmpty(mUserQuery));
    442 
    443             // In order to properly configure the input method (if one is being used), we
    444             // need to let it know if we'll be providing suggestions.  Although it would be
    445             // difficult/expensive to know if every last detail has been configured properly, we
    446             // can at least see if a suggestions provider has been configured, and use that
    447             // as our trigger.
    448             int inputType = mSearchable.getInputType();
    449             // We only touch this if the input type is set up for text (which it almost certainly
    450             // should be, in the case of search!)
    451             if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
    452                 // The existence of a suggestions authority is the proxy for "suggestions
    453                 // are available here"
    454                 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
    455                 if (mSearchable.getSuggestAuthority() != null) {
    456                     inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
    457                 }
    458             }
    459             mSearchAutoComplete.setInputType(inputType);
    460             mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
    461             mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
    462 
    463             // If the search dialog is going to show a voice search button, then don't let
    464             // the soft keyboard display a microphone button if it would have otherwise.
    465             if (mSearchable.getVoiceSearchEnabled()) {
    466                 mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
    467             } else {
    468                 mSearchAutoComplete.setPrivateImeOptions(null);
    469             }
    470         }
    471     }
    472 
    473     /**
    474      * Updates the auto-complete text view.
    475      */
    476     private void updateSearchAutoComplete() {
    477         // close any existing suggestions adapter
    478         closeSuggestionsAdapter();
    479 
    480         mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
    481         mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
    482         // we dismiss the entire dialog instead
    483         mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
    484 
    485         mSearchAutoComplete.setForceIgnoreOutsideTouch(true);
    486 
    487         // attach the suggestions adapter, if suggestions are available
    488         // The existence of a suggestions authority is the proxy for "suggestions available here"
    489         if (mSearchable.getSuggestAuthority() != null) {
    490             mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable,
    491                     mOutsideDrawablesCache);
    492             mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
    493         }
    494     }
    495 
    496     private void updateSearchButton() {
    497         String textLabel = null;
    498         Drawable iconLabel = null;
    499         int textId = mSearchable.getSearchButtonText();
    500         if (isBrowserSearch()){
    501             iconLabel = getContext().getResources()
    502                     .getDrawable(com.android.internal.R.drawable.ic_btn_search_go);
    503         } else if (textId != 0) {
    504             textLabel = mActivityContext.getResources().getString(textId);
    505         } else {
    506             iconLabel = getContext().getResources().
    507                     getDrawable(com.android.internal.R.drawable.ic_btn_search);
    508         }
    509         mGoButton.setText(textLabel);
    510         mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
    511     }
    512 
    513     private void updateSearchAppIcon() {
    514         if (isBrowserSearch()) {
    515             mAppIcon.setImageResource(0);
    516             mAppIcon.setVisibility(View.GONE);
    517             mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
    518                     mSearchPlate.getPaddingTop(),
    519                     mSearchPlate.getPaddingRight(),
    520                     mSearchPlate.getPaddingBottom());
    521         } else {
    522             PackageManager pm = getContext().getPackageManager();
    523             Drawable icon;
    524             try {
    525                 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
    526                 icon = pm.getApplicationIcon(info.applicationInfo);
    527                 if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
    528             } catch (NameNotFoundException e) {
    529                 icon = pm.getDefaultActivityIcon();
    530                 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
    531             }
    532             mAppIcon.setImageDrawable(icon);
    533             mAppIcon.setVisibility(View.VISIBLE);
    534             mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
    535                     mSearchPlate.getPaddingTop(),
    536                     mSearchPlate.getPaddingRight(),
    537                     mSearchPlate.getPaddingBottom());
    538         }
    539     }
    540 
    541     /**
    542      * Setup the search "Badge" if requested by mode flags.
    543      */
    544     private void updateSearchBadge() {
    545         // assume both hidden
    546         int visibility = View.GONE;
    547         Drawable icon = null;
    548         CharSequence text = null;
    549 
    550         // optionally show one or the other.
    551         if (mSearchable.useBadgeIcon()) {
    552             icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
    553             visibility = View.VISIBLE;
    554             if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
    555         } else if (mSearchable.useBadgeLabel()) {
    556             text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
    557             visibility = View.VISIBLE;
    558             if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
    559         }
    560 
    561         mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
    562         mBadgeLabel.setText(text);
    563         mBadgeLabel.setVisibility(visibility);
    564     }
    565 
    566     /**
    567      * Update the hint in the query text field.
    568      */
    569     private void updateQueryHint() {
    570         if (isShowing()) {
    571             String hint = null;
    572             if (mSearchable != null) {
    573                 int hintId = mSearchable.getHintId();
    574                 if (hintId != 0) {
    575                     hint = mActivityContext.getString(hintId);
    576                 }
    577             }
    578             mSearchAutoComplete.setHint(hint);
    579         }
    580     }
    581 
    582     /**
    583      * Update the visibility of the voice button.  There are actually two voice search modes,
    584      * either of which will activate the button.
    585      * @param empty whether the search query text field is empty. If it is, then the other
    586      * criteria apply to make the voice button visible. Otherwise the voice button will not
    587      * be visible - i.e., if the user has typed a query, remove the voice button.
    588      */
    589     private void updateVoiceButton(boolean empty) {
    590         int visibility = View.GONE;
    591         if ((mAppSearchData == null || !mAppSearchData.getBoolean(
    592                 SearchManager.DISABLE_VOICE_SEARCH, false))
    593                 && mSearchable.getVoiceSearchEnabled() && empty) {
    594             Intent testIntent = null;
    595             if (mSearchable.getVoiceSearchLaunchWebSearch()) {
    596                 testIntent = mVoiceWebSearchIntent;
    597             } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
    598                 testIntent = mVoiceAppSearchIntent;
    599             }
    600             if (testIntent != null) {
    601                 ResolveInfo ri = getContext().getPackageManager().
    602                         resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
    603                 if (ri != null) {
    604                     visibility = View.VISIBLE;
    605                 }
    606             }
    607         }
    608         mVoiceButton.setVisibility(visibility);
    609     }
    610 
    611     /** Called by SuggestionsAdapter when the cursor contents changed. */
    612     void onDataSetChanged() {
    613         if (mSearchAutoComplete != null && mSuggestionsAdapter != null) {
    614             mSearchAutoComplete.onFilterComplete(mSuggestionsAdapter.getCount());
    615         }
    616     }
    617 
    618     /**
    619      * Hack to determine whether this is the browser, so we can adjust the UI.
    620      */
    621     private boolean isBrowserSearch() {
    622         return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/");
    623     }
    624 
    625     /*
    626      * Listeners of various types
    627      */
    628 
    629     /**
    630      * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
    631      * touch is outside the window. But the window includes space for the drop-down,
    632      * so we also cancel on taps outside the search bar when the drop-down is not showing.
    633      */
    634     @Override
    635     public boolean onTouchEvent(MotionEvent event) {
    636         // cancel if the drop-down is not showing and the touch event was outside the search plate
    637         if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
    638             if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
    639             cancel();
    640             return true;
    641         }
    642         // Let Dialog handle events outside the window while the pop-up is showing.
    643         return super.onTouchEvent(event);
    644     }
    645 
    646     private boolean isOutOfBounds(View v, MotionEvent event) {
    647         final int x = (int) event.getX();
    648         final int y = (int) event.getY();
    649         final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
    650         return (x < -slop) || (y < -slop)
    651                 || (x > (v.getWidth()+slop))
    652                 || (y > (v.getHeight()+slop));
    653     }
    654 
    655     /**
    656      * Dialog's OnKeyListener implements various search-specific functionality
    657      *
    658      * @param keyCode This is the keycode of the typed key, and is the same value as
    659      *        found in the KeyEvent parameter.
    660      * @param event The complete event record for the typed key
    661      *
    662      * @return Return true if the event was handled here, or false if not.
    663      */
    664     @Override
    665     public boolean onKeyDown(int keyCode, KeyEvent event) {
    666         if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
    667         if (mSearchable == null) {
    668             return false;
    669         }
    670 
    671         // if it's an action specified by the searchable activity, launch the
    672         // entered query with the action key
    673         SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    674         if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
    675             launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
    676             return true;
    677         }
    678 
    679         return super.onKeyDown(keyCode, event);
    680     }
    681 
    682     /**
    683      * Callback to watch the textedit field for empty/non-empty
    684      */
    685     private TextWatcher mTextWatcher = new TextWatcher() {
    686 
    687         public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
    688 
    689         public void onTextChanged(CharSequence s, int start,
    690                 int before, int after) {
    691             if (DBG_LOG_TIMING) {
    692                 dbgLogTiming("onTextChanged()");
    693             }
    694             if (mSearchable == null) {
    695                 return;
    696             }
    697             if (!mSearchAutoComplete.isPerformingCompletion()) {
    698                 // The user changed the query, remember it.
    699                 mUserQuery = s == null ? "" : s.toString();
    700             }
    701             updateWidgetState();
    702             // Always want to show the microphone if the context is voice.
    703             // Also show the microphone if this is a browser search and the
    704             // query matches the initial query.
    705             updateVoiceButton(mSearchAutoComplete.isEmpty()
    706                     || (isBrowserSearch() && mInitialQuery.equals(mUserQuery))
    707                     || (mAppSearchData != null && mAppSearchData.getBoolean(
    708                     SearchManager.CONTEXT_IS_VOICE)));
    709         }
    710 
    711         public void afterTextChanged(Editable s) {
    712             if (mSearchable == null) {
    713                 return;
    714             }
    715             if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) {
    716                 // The user changed the query, check if it is a URL and if so change the search
    717                 // button in the soft keyboard to the 'Go' button.
    718                 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION))
    719                         | EditorInfo.IME_ACTION_GO;
    720                 if (options != mSearchAutoCompleteImeOptions) {
    721                     mSearchAutoCompleteImeOptions = options;
    722                     mSearchAutoComplete.setImeOptions(options);
    723                     // This call is required to update the soft keyboard UI with latest IME flags.
    724                     mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
    725                 }
    726             }
    727         }
    728     };
    729 
    730     /**
    731      * Enable/Disable the go button based on edit text state (any text?)
    732      */
    733     private void updateWidgetState() {
    734         // enable the button if we have one or more non-space characters
    735         boolean enabled = !mSearchAutoComplete.isEmpty();
    736         if (isBrowserSearch()) {
    737             // In the browser, we hide the search button when there is no text,
    738             // or if the text matches the initial query.
    739             if (enabled && !mInitialQuery.equals(mUserQuery)) {
    740                 mSearchAutoComplete.setBackgroundResource(
    741                         com.android.internal.R.drawable.textfield_search);
    742                 mGoButton.setVisibility(View.VISIBLE);
    743                 // Just to be sure
    744                 mGoButton.setEnabled(true);
    745                 mGoButton.setFocusable(true);
    746             } else {
    747                 mSearchAutoComplete.setBackgroundResource(
    748                         com.android.internal.R.drawable.textfield_search_empty);
    749                 mGoButton.setVisibility(View.GONE);
    750             }
    751         } else {
    752             // Elsewhere we just disable the button
    753             mGoButton.setEnabled(enabled);
    754             mGoButton.setFocusable(enabled);
    755         }
    756     }
    757 
    758     /**
    759      * React to typing in the GO search button by refocusing to EditText.
    760      * Continue typing the query.
    761      */
    762     View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
    763         public boolean onKey(View v, int keyCode, KeyEvent event) {
    764             // guard against possible race conditions
    765             if (mSearchable == null) {
    766                 return false;
    767             }
    768 
    769             if (!event.isSystem() &&
    770                     (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
    771                     (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
    772                     (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
    773                     (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
    774                 // restore focus and give key to EditText ...
    775                 if (mSearchAutoComplete.requestFocus()) {
    776                     return mSearchAutoComplete.dispatchKeyEvent(event);
    777                 }
    778             }
    779 
    780             return false;
    781         }
    782     };
    783 
    784     /**
    785      * React to a click in the GO button by launching a search.
    786      */
    787     View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
    788         public void onClick(View v) {
    789             // guard against possible race conditions
    790             if (mSearchable == null) {
    791                 return;
    792             }
    793             launchQuerySearch();
    794         }
    795     };
    796 
    797     /**
    798      * React to a click in the voice search button.
    799      */
    800     View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
    801         public void onClick(View v) {
    802             // guard against possible race conditions
    803             if (mSearchable == null) {
    804                 return;
    805             }
    806             SearchableInfo searchable = mSearchable;
    807             try {
    808                 if (searchable.getVoiceSearchLaunchWebSearch()) {
    809                     Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
    810                             searchable);
    811                     getContext().startActivity(webSearchIntent);
    812                 } else if (searchable.getVoiceSearchLaunchRecognizer()) {
    813                     Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
    814                             searchable);
    815                     getContext().startActivity(appSearchIntent);
    816                 }
    817             } catch (ActivityNotFoundException e) {
    818                 // Should not happen, since we check the availability of
    819                 // voice search before showing the button. But just in case...
    820                 Log.w(LOG_TAG, "Could not find voice search activity");
    821             }
    822             dismiss();
    823          }
    824     };
    825 
    826     /**
    827      * Create and return an Intent that can launch the voice search activity for web search.
    828      */
    829     private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
    830         Intent voiceIntent = new Intent(baseIntent);
    831         ComponentName searchActivity = searchable.getSearchActivity();
    832         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
    833                 searchActivity == null ? null : searchActivity.flattenToShortString());
    834         return voiceIntent;
    835     }
    836 
    837     /**
    838      * Create and return an Intent that can launch the voice search activity, perform a specific
    839      * voice transcription, and forward the results to the searchable activity.
    840      *
    841      * @param baseIntent The voice app search intent to start from
    842      * @return A completely-configured intent ready to send to the voice search activity
    843      */
    844     private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
    845         ComponentName searchActivity = searchable.getSearchActivity();
    846 
    847         // create the necessary intent to set up a search-and-forward operation
    848         // in the voice search system.   We have to keep the bundle separate,
    849         // because it becomes immutable once it enters the PendingIntent
    850         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
    851         queryIntent.setComponent(searchActivity);
    852         PendingIntent pending = PendingIntent.getActivity(
    853                 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
    854 
    855         // Now set up the bundle that will be inserted into the pending intent
    856         // when it's time to do the search.  We always build it here (even if empty)
    857         // because the voice search activity will always need to insert "QUERY" into
    858         // it anyway.
    859         Bundle queryExtras = new Bundle();
    860         if (mAppSearchData != null) {
    861             queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
    862         }
    863 
    864         // Now build the intent to launch the voice search.  Add all necessary
    865         // extras to launch the voice recognizer, and then all the necessary extras
    866         // to forward the results to the searchable activity
    867         Intent voiceIntent = new Intent(baseIntent);
    868 
    869         // Add all of the configuration options supplied by the searchable's metadata
    870         String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
    871         String prompt = null;
    872         String language = null;
    873         int maxResults = 1;
    874         Resources resources = mActivityContext.getResources();
    875         if (searchable.getVoiceLanguageModeId() != 0) {
    876             languageModel = resources.getString(searchable.getVoiceLanguageModeId());
    877         }
    878         if (searchable.getVoicePromptTextId() != 0) {
    879             prompt = resources.getString(searchable.getVoicePromptTextId());
    880         }
    881         if (searchable.getVoiceLanguageId() != 0) {
    882             language = resources.getString(searchable.getVoiceLanguageId());
    883         }
    884         if (searchable.getVoiceMaxResults() != 0) {
    885             maxResults = searchable.getVoiceMaxResults();
    886         }
    887         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
    888         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
    889         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
    890         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
    891         voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
    892                 searchActivity == null ? null : searchActivity.flattenToShortString());
    893 
    894         // Add the values that configure forwarding the results
    895         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
    896         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
    897 
    898         return voiceIntent;
    899     }
    900 
    901     /**
    902      * Corrects http/https typo errors in the given url string, and if the protocol specifier was
    903      * not present defaults to http.
    904      *
    905      * @param inUrl URL to check and fix
    906      * @return fixed URL string.
    907      */
    908     private String fixUrl(String inUrl) {
    909         if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
    910             return inUrl;
    911 
    912         if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
    913             if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
    914                 inUrl = inUrl.replaceFirst("/", "//");
    915             } else {
    916                 inUrl = inUrl.replaceFirst(":", "://");
    917             }
    918         }
    919 
    920         if (inUrl.indexOf("://") == -1) {
    921             inUrl = "http://" + inUrl;
    922         }
    923 
    924         return inUrl;
    925     }
    926 
    927     /**
    928      * React to the user typing "enter" or other hardwired keys while typing in the search box.
    929      * This handles these special keys while the edit box has focus.
    930      */
    931     View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
    932         public boolean onKey(View v, int keyCode, KeyEvent event) {
    933             // guard against possible race conditions
    934             if (mSearchable == null) {
    935                 return false;
    936             }
    937 
    938             if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
    939             if (DBG) {
    940                 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
    941                         + "), selection: " + mSearchAutoComplete.getListSelection());
    942             }
    943 
    944             // If a suggestion is selected, handle enter, search key, and action keys
    945             // as presses on the selected suggestion
    946             if (mSearchAutoComplete.isPopupShowing() &&
    947                     mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
    948                 return onSuggestionsKey(v, keyCode, event);
    949             }
    950 
    951             // If there is text in the query box, handle enter, and action keys
    952             // The search key is handled by the dialog's onKeyDown().
    953             if (!mSearchAutoComplete.isEmpty()) {
    954                 if (keyCode == KeyEvent.KEYCODE_ENTER
    955                         && event.getAction() == KeyEvent.ACTION_UP) {
    956                     v.cancelLongPress();
    957 
    958                     // If this is a url entered by the user & we displayed the 'Go' button which
    959                     // the user clicked, launch the url instead of using it as a search query.
    960                     if (mSearchable.autoUrlDetect() &&
    961                         (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
    962                                 == EditorInfo.IME_ACTION_GO) {
    963                         Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
    964                         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    965                         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    966                         launchIntent(intent);
    967                     } else {
    968                         // Launch as a regular search.
    969                         launchQuerySearch();
    970                     }
    971                     return true;
    972                 }
    973                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
    974                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
    975                     if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
    976                         launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
    977                         return true;
    978                     }
    979                 }
    980             }
    981             return false;
    982         }
    983     };
    984 
    985     @Override
    986     public void hide() {
    987         if (!isShowing()) return;
    988 
    989         // We made sure the IME was displayed, so also make sure it is closed
    990         // when we go away.
    991         InputMethodManager imm = (InputMethodManager)getContext()
    992                 .getSystemService(Context.INPUT_METHOD_SERVICE);
    993         if (imm != null) {
    994             imm.hideSoftInputFromWindow(
    995                     getWindow().getDecorView().getWindowToken(), 0);
    996         }
    997 
    998         super.hide();
    999     }
   1000 
   1001     /**
   1002      * React to the user typing while in the suggestions list. First, check for action
   1003      * keys. If not handled, try refocusing regular characters into the EditText.
   1004      */
   1005     private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
   1006         // guard against possible race conditions (late arrival after dismiss)
   1007         if (mSearchable == null) {
   1008             return false;
   1009         }
   1010         if (mSuggestionsAdapter == null) {
   1011             return false;
   1012         }
   1013         if (event.getAction() == KeyEvent.ACTION_DOWN) {
   1014             if (DBG_LOG_TIMING) {
   1015                 dbgLogTiming("onSuggestionsKey()");
   1016             }
   1017 
   1018             // First, check for enter or search (both of which we'll treat as a "click")
   1019             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
   1020                 int position = mSearchAutoComplete.getListSelection();
   1021                 return launchSuggestion(position);
   1022             }
   1023 
   1024             // Next, check for left/right moves, which we use to "return" the user to the edit view
   1025             if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
   1026                 // give "focus" to text editor, with cursor at the beginning if
   1027                 // left key, at end if right key
   1028                 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
   1029                 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
   1030                         0 : mSearchAutoComplete.length();
   1031                 mSearchAutoComplete.setSelection(selPoint);
   1032                 mSearchAutoComplete.setListSelection(0);
   1033                 mSearchAutoComplete.clearListSelection();
   1034                 mSearchAutoComplete.ensureImeVisible(true);
   1035 
   1036                 return true;
   1037             }
   1038 
   1039             // Next, check for an "up and out" move
   1040             if (keyCode == KeyEvent.KEYCODE_DPAD_UP
   1041                     && 0 == mSearchAutoComplete.getListSelection()) {
   1042                 restoreUserQuery();
   1043                 // let ACTV complete the move
   1044                 return false;
   1045             }
   1046 
   1047             // Next, check for an "action key"
   1048             SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
   1049             if ((actionKey != null) &&
   1050                     ((actionKey.getSuggestActionMsg() != null) ||
   1051                      (actionKey.getSuggestActionMsgColumn() != null))) {
   1052                 // launch suggestion using action key column
   1053                 int position = mSearchAutoComplete.getListSelection();
   1054                 if (position != ListView.INVALID_POSITION) {
   1055                     Cursor c = mSuggestionsAdapter.getCursor();
   1056                     if (c.moveToPosition(position)) {
   1057                         final String actionMsg = getActionKeyMessage(c, actionKey);
   1058                         if (actionMsg != null && (actionMsg.length() > 0)) {
   1059                             return launchSuggestion(position, keyCode, actionMsg);
   1060                         }
   1061                     }
   1062                 }
   1063             }
   1064         }
   1065         return false;
   1066     }
   1067 
   1068     /**
   1069      * Launch a search for the text in the query text field.
   1070      */
   1071     public void launchQuerySearch()  {
   1072         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
   1073     }
   1074 
   1075     /**
   1076      * Launch a search for the text in the query text field.
   1077      *
   1078      * @param actionKey The key code of the action key that was pressed,
   1079      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1080      * @param actionMsg The message for the action key that was pressed,
   1081      *        or <code>null</code> if none.
   1082      */
   1083     protected void launchQuerySearch(int actionKey, String actionMsg)  {
   1084         String query = mSearchAutoComplete.getText().toString();
   1085         String action = Intent.ACTION_SEARCH;
   1086         Intent intent = createIntent(action, null, null, query, null,
   1087                 actionKey, actionMsg);
   1088         launchIntent(intent);
   1089     }
   1090 
   1091     /**
   1092      * Launches an intent based on a suggestion.
   1093      *
   1094      * @param position The index of the suggestion to create the intent from.
   1095      * @return true if a successful launch, false if could not (e.g. bad position).
   1096      */
   1097     protected boolean launchSuggestion(int position) {
   1098         return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
   1099     }
   1100 
   1101     /**
   1102      * Launches an intent based on a suggestion.
   1103      *
   1104      * @param position The index of the suggestion to create the intent from.
   1105      * @param actionKey The key code of the action key that was pressed,
   1106      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1107      * @param actionMsg The message for the action key that was pressed,
   1108      *        or <code>null</code> if none.
   1109      * @return true if a successful launch, false if could not (e.g. bad position).
   1110      */
   1111     protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
   1112         Cursor c = mSuggestionsAdapter.getCursor();
   1113         if ((c != null) && c.moveToPosition(position)) {
   1114 
   1115             Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
   1116 
   1117            // launch the intent
   1118             launchIntent(intent);
   1119 
   1120             return true;
   1121         }
   1122         return false;
   1123     }
   1124 
   1125     /**
   1126      * Launches an intent, including any special intent handling.
   1127      */
   1128     private void launchIntent(Intent intent) {
   1129         if (intent == null) {
   1130             return;
   1131         }
   1132         Log.d(LOG_TAG, "launching " + intent);
   1133         try {
   1134             // If the intent was created from a suggestion, it will always have an explicit
   1135             // component here.
   1136             Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI());
   1137             getContext().startActivity(intent);
   1138             // If the search switches to a different activity,
   1139             // SearchDialogWrapper#performActivityResuming
   1140             // will handle hiding the dialog when the next activity starts, but for
   1141             // real in-app search, we still need to dismiss the dialog.
   1142             dismiss();
   1143         } catch (RuntimeException ex) {
   1144             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
   1145         }
   1146     }
   1147 
   1148     /**
   1149      * If the intent is to open an HTTP or HTTPS URL, we set
   1150      * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that
   1151      * has been opened by us for the same URL will be reused.
   1152      */
   1153     private void setBrowserApplicationId(Intent intent) {
   1154         Uri data = intent.getData();
   1155         if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) {
   1156             String scheme = data.getScheme();
   1157             if (scheme != null && scheme.startsWith("http")) {
   1158                 intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString());
   1159             }
   1160         }
   1161     }
   1162 
   1163     /**
   1164      * Sets the list item selection in the AutoCompleteTextView's ListView.
   1165      */
   1166     public void setListSelection(int index) {
   1167         mSearchAutoComplete.setListSelection(index);
   1168     }
   1169 
   1170     /**
   1171      * When a particular suggestion has been selected, perform the various lookups required
   1172      * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
   1173      * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
   1174      * the suggestion includes a data id.
   1175      *
   1176      * @param c The suggestions cursor, moved to the row of the user's selection
   1177      * @param actionKey The key code of the action key that was pressed,
   1178      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1179      * @param actionMsg The message for the action key that was pressed,
   1180      *        or <code>null</code> if none.
   1181      * @return An intent for the suggestion at the cursor's position.
   1182      */
   1183     private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
   1184         try {
   1185             // use specific action if supplied, or default action if supplied, or fixed default
   1186             String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
   1187 
   1188             // some items are display only, or have effect via the cursor respond click reporting.
   1189             if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
   1190                 return null;
   1191             }
   1192 
   1193             if (action == null) {
   1194                 action = mSearchable.getSuggestIntentAction();
   1195             }
   1196             if (action == null) {
   1197                 action = Intent.ACTION_SEARCH;
   1198             }
   1199 
   1200             // use specific data if supplied, or default data if supplied
   1201             String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
   1202             if (data == null) {
   1203                 data = mSearchable.getSuggestIntentData();
   1204             }
   1205             // then, if an ID was provided, append it.
   1206             if (data != null) {
   1207                 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
   1208                 if (id != null) {
   1209                     data = data + "/" + Uri.encode(id);
   1210                 }
   1211             }
   1212             Uri dataUri = (data == null) ? null : Uri.parse(data);
   1213 
   1214             String componentName = getColumnString(
   1215                     c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
   1216 
   1217             String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
   1218             String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
   1219 
   1220             return createIntent(action, dataUri, extraData, query, componentName, actionKey,
   1221                     actionMsg);
   1222         } catch (RuntimeException e ) {
   1223             int rowNum;
   1224             try {                       // be really paranoid now
   1225                 rowNum = c.getPosition();
   1226             } catch (RuntimeException e2 ) {
   1227                 rowNum = -1;
   1228             }
   1229             Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
   1230                             " returned exception" + e.toString());
   1231             return null;
   1232         }
   1233     }
   1234 
   1235     /**
   1236      * Constructs an intent from the given information and the search dialog state.
   1237      *
   1238      * @param action Intent action.
   1239      * @param data Intent data, or <code>null</code>.
   1240      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
   1241      * @param query Intent query, or <code>null</code>.
   1242      * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
   1243      * @param actionKey The key code of the action key that was pressed,
   1244      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
   1245      * @param actionMsg The message for the action key that was pressed,
   1246      *        or <code>null</code> if none.
   1247      * @param mode The search mode, one of the acceptable values for
   1248      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
   1249      * @return The intent.
   1250      */
   1251     private Intent createIntent(String action, Uri data, String extraData, String query,
   1252             String componentName, int actionKey, String actionMsg) {
   1253         // Now build the Intent
   1254         Intent intent = new Intent(action);
   1255         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   1256         // We need CLEAR_TOP to avoid reusing an old task that has other activities
   1257         // on top of the one we want. We don't want to do this in in-app search though,
   1258         // as it can be destructive to the activity stack.
   1259         if (data != null) {
   1260             intent.setData(data);
   1261         }
   1262         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
   1263         if (query != null) {
   1264             intent.putExtra(SearchManager.QUERY, query);
   1265         }
   1266         if (extraData != null) {
   1267             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
   1268         }
   1269         if (mAppSearchData != null) {
   1270             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
   1271         }
   1272         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
   1273             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
   1274             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
   1275         }
   1276         intent.setComponent(mSearchable.getSearchActivity());
   1277         return intent;
   1278     }
   1279 
   1280     /**
   1281      * For a given suggestion and a given cursor row, get the action message.  If not provided
   1282      * by the specific row/column, also check for a single definition (for the action key).
   1283      *
   1284      * @param c The cursor providing suggestions
   1285      * @param actionKey The actionkey record being examined
   1286      *
   1287      * @return Returns a string, or null if no action key message for this suggestion
   1288      */
   1289     private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
   1290         String result = null;
   1291         // check first in the cursor data, for a suggestion-specific message
   1292         final String column = actionKey.getSuggestActionMsgColumn();
   1293         if (column != null) {
   1294             result = SuggestionsAdapter.getColumnString(c, column);
   1295         }
   1296         // If the cursor didn't give us a message, see if there's a single message defined
   1297         // for the actionkey (for all suggestions)
   1298         if (result == null) {
   1299             result = actionKey.getSuggestActionMsg();
   1300         }
   1301         return result;
   1302     }
   1303 
   1304     /**
   1305      * The root element in the search bar layout. This is a custom view just to override
   1306      * the handling of the back button.
   1307      */
   1308     public static class SearchBar extends LinearLayout {
   1309 
   1310         private SearchDialog mSearchDialog;
   1311 
   1312         public SearchBar(Context context, AttributeSet attrs) {
   1313             super(context, attrs);
   1314         }
   1315 
   1316         public SearchBar(Context context) {
   1317             super(context);
   1318         }
   1319 
   1320         public void setSearchDialog(SearchDialog searchDialog) {
   1321             mSearchDialog = searchDialog;
   1322         }
   1323 
   1324         /**
   1325          * Overrides the handling of the back key to move back to the previous sources or dismiss
   1326          * the search dialog, instead of dismissing the input method.
   1327          */
   1328         @Override
   1329         public boolean dispatchKeyEventPreIme(KeyEvent event) {
   1330             if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")");
   1331             if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
   1332                 KeyEvent.DispatcherState state = getKeyDispatcherState();
   1333                 if (state != null) {
   1334                     if (event.getAction() == KeyEvent.ACTION_DOWN
   1335                             && event.getRepeatCount() == 0) {
   1336                         state.startTracking(event, this);
   1337                         return true;
   1338                     } else if (event.getAction() == KeyEvent.ACTION_UP
   1339                             && !event.isCanceled() && state.isTracking(event)) {
   1340                         mSearchDialog.onBackPressed();
   1341                         return true;
   1342                     }
   1343                 }
   1344             }
   1345             return super.dispatchKeyEventPreIme(event);
   1346         }
   1347     }
   1348 
   1349     /**
   1350      * Local subclass for AutoCompleteTextView.
   1351      */
   1352     public static class SearchAutoComplete extends AutoCompleteTextView {
   1353 
   1354         private int mThreshold;
   1355 
   1356         public SearchAutoComplete(Context context) {
   1357             super(context);
   1358             mThreshold = getThreshold();
   1359         }
   1360 
   1361         public SearchAutoComplete(Context context, AttributeSet attrs) {
   1362             super(context, attrs);
   1363             mThreshold = getThreshold();
   1364         }
   1365 
   1366         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
   1367             super(context, attrs, defStyle);
   1368             mThreshold = getThreshold();
   1369         }
   1370 
   1371         @Override
   1372         public void setThreshold(int threshold) {
   1373             super.setThreshold(threshold);
   1374             mThreshold = threshold;
   1375         }
   1376 
   1377         /**
   1378          * Returns true if the text field is empty, or contains only whitespace.
   1379          */
   1380         private boolean isEmpty() {
   1381             return TextUtils.getTrimmedLength(getText()) == 0;
   1382         }
   1383 
   1384         /**
   1385          * We override this method to avoid replacing the query box text
   1386          * when a suggestion is clicked.
   1387          */
   1388         @Override
   1389         protected void replaceText(CharSequence text) {
   1390         }
   1391 
   1392         /**
   1393          * We override this method to avoid an extra onItemClick being called on the
   1394          * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)}
   1395          * when an item is clicked with the trackball.
   1396          */
   1397         @Override
   1398         public void performCompletion() {
   1399         }
   1400 
   1401         /**
   1402          * We override this method to be sure and show the soft keyboard if appropriate when
   1403          * the TextView has focus.
   1404          */
   1405         @Override
   1406         public void onWindowFocusChanged(boolean hasWindowFocus) {
   1407             super.onWindowFocusChanged(hasWindowFocus);
   1408 
   1409             if (hasWindowFocus) {
   1410                 InputMethodManager inputManager = (InputMethodManager)
   1411                         getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
   1412                 inputManager.showSoftInput(this, 0);
   1413                 // If in landscape mode, then make sure that
   1414                 // the ime is in front of the dropdown.
   1415                 if (isLandscapeMode(getContext())) {
   1416                     ensureImeVisible(true);
   1417                 }
   1418             }
   1419         }
   1420 
   1421         /**
   1422          * We override this method so that we can allow a threshold of zero, which ACTV does not.
   1423          */
   1424         @Override
   1425         public boolean enoughToFilter() {
   1426             return mThreshold <= 0 || super.enoughToFilter();
   1427         }
   1428 
   1429     }
   1430 
   1431     @Override
   1432     public void onBackPressed() {
   1433         // If the input method is covering the search dialog completely,
   1434         // e.g. in landscape mode with no hard keyboard, dismiss just the input method
   1435         InputMethodManager imm = (InputMethodManager)getContext()
   1436                 .getSystemService(Context.INPUT_METHOD_SERVICE);
   1437         if (imm != null && imm.isFullscreenMode() &&
   1438                 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) {
   1439             return;
   1440         }
   1441         // Close search dialog
   1442         cancel();
   1443     }
   1444 
   1445     /**
   1446      * Implements OnItemClickListener
   1447      */
   1448     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   1449         if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
   1450         launchSuggestion(position);
   1451     }
   1452 
   1453     /**
   1454      * Implements OnItemSelectedListener
   1455      */
   1456      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
   1457          if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
   1458          // A suggestion has been selected, rewrite the query if possible,
   1459          // otherwise the restore the original query.
   1460          if (REWRITE_QUERIES) {
   1461              rewriteQueryFromSuggestion(position);
   1462          }
   1463      }
   1464 
   1465      /**
   1466       * Implements OnItemSelectedListener
   1467       */
   1468      public void onNothingSelected(AdapterView<?> parent) {
   1469          if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
   1470      }
   1471 
   1472      /**
   1473       * Query rewriting.
   1474       */
   1475 
   1476      private void rewriteQueryFromSuggestion(int position) {
   1477          Cursor c = mSuggestionsAdapter.getCursor();
   1478          if (c == null) {
   1479              return;
   1480          }
   1481          if (c.moveToPosition(position)) {
   1482              // Get the new query from the suggestion.
   1483              CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
   1484              if (newQuery != null) {
   1485                  // The suggestion rewrites the query.
   1486                  if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
   1487                  // Update the text field, without getting new suggestions.
   1488                  setQuery(newQuery);
   1489              } else {
   1490                  // The suggestion does not rewrite the query, restore the user's query.
   1491                  if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
   1492                  restoreUserQuery();
   1493              }
   1494          } else {
   1495              // We got a bad position, restore the user's query.
   1496              Log.w(LOG_TAG, "Bad suggestion position: " + position);
   1497              restoreUserQuery();
   1498          }
   1499      }
   1500 
   1501      /**
   1502       * Restores the query entered by the user if needed.
   1503       */
   1504      private void restoreUserQuery() {
   1505          if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
   1506          setQuery(mUserQuery);
   1507      }
   1508 
   1509      /**
   1510       * Sets the text in the query box, without updating the suggestions.
   1511       */
   1512      private void setQuery(CharSequence query) {
   1513          mSearchAutoComplete.setText(query, false);
   1514          if (query != null) {
   1515              mSearchAutoComplete.setSelection(query.length());
   1516          }
   1517      }
   1518 
   1519      /**
   1520       * Sets the text in the query box, updating the suggestions.
   1521       */
   1522      private void setUserQuery(String query) {
   1523          if (query == null) {
   1524              query = "";
   1525          }
   1526          mUserQuery = query;
   1527          mSearchAutoComplete.setText(query);
   1528          mSearchAutoComplete.setSelection(query.length());
   1529      }
   1530 
   1531     /**
   1532      * Debugging Support
   1533      */
   1534 
   1535     /**
   1536      * For debugging only, sample the millisecond clock and log it.
   1537      * Uses AtomicLong so we can use in multiple threads
   1538      */
   1539     private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
   1540     private void dbgLogTiming(final String caller) {
   1541         long millis = SystemClock.uptimeMillis();
   1542         long oldTime = mLastLogTime.getAndSet(millis);
   1543         long delta = millis - oldTime;
   1544         final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
   1545         Log.d(LOG_TAG,report);
   1546     }
   1547 }
   1548