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 android.content.BroadcastReceiver;
     21 import android.content.ComponentName;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.content.pm.ActivityInfo;
     26 import android.content.pm.PackageManager;
     27 import android.content.pm.PackageManager.NameNotFoundException;
     28 import android.content.res.Configuration;
     29 import android.graphics.drawable.Drawable;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.speech.RecognizerIntent;
     33 import android.text.InputType;
     34 import android.text.TextUtils;
     35 import android.util.AttributeSet;
     36 import android.util.Log;
     37 import android.util.TypedValue;
     38 import android.view.ActionMode;
     39 import android.view.Gravity;
     40 import android.view.KeyEvent;
     41 import android.view.MotionEvent;
     42 import android.view.View;
     43 import android.view.ViewConfiguration;
     44 import android.view.ViewGroup;
     45 import android.view.Window;
     46 import android.view.WindowManager;
     47 import android.view.inputmethod.InputMethodManager;
     48 import android.widget.AutoCompleteTextView;
     49 import android.widget.ImageView;
     50 import android.widget.LinearLayout;
     51 import android.widget.SearchView;
     52 import android.widget.TextView;
     53 
     54 /**
     55  * Search dialog. This is controlled by the
     56  * SearchManager and runs in the current foreground process.
     57  *
     58  * @hide
     59  */
     60 public class SearchDialog extends Dialog {
     61 
     62     // Debugging support
     63     private static final boolean DBG = false;
     64     private static final String LOG_TAG = "SearchDialog";
     65 
     66     private static final String INSTANCE_KEY_COMPONENT = "comp";
     67     private static final String INSTANCE_KEY_APPDATA = "data";
     68     private static final String INSTANCE_KEY_USER_QUERY = "uQry";
     69 
     70     // The string used for privateImeOptions to identify to the IME that it should not show
     71     // a microphone button since one already exists in the search dialog.
     72     private static final String IME_OPTION_NO_MICROPHONE = "nm";
     73 
     74     private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
     75 
     76     // views & widgets
     77     private TextView mBadgeLabel;
     78     private ImageView mAppIcon;
     79     private AutoCompleteTextView mSearchAutoComplete;
     80     private View mSearchPlate;
     81     private SearchView mSearchView;
     82     private Drawable mWorkingSpinner;
     83     private View mCloseSearch;
     84 
     85     // interaction with searchable application
     86     private SearchableInfo mSearchable;
     87     private ComponentName mLaunchComponent;
     88     private Bundle mAppSearchData;
     89     private Context mActivityContext;
     90 
     91     // For voice searching
     92     private final Intent mVoiceWebSearchIntent;
     93     private final Intent mVoiceAppSearchIntent;
     94 
     95     // The query entered by the user. This is not changed when selecting a suggestion
     96     // that modifies the contents of the text field. But if the user then edits
     97     // the suggestion, the resulting string is saved.
     98     private String mUserQuery;
     99 
    100     // Last known IME options value for the search edit text.
    101     private int mSearchAutoCompleteImeOptions;
    102 
    103     private BroadcastReceiver mConfChangeListener = new BroadcastReceiver() {
    104         @Override
    105         public void onReceive(Context context, Intent intent) {
    106             if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
    107                 onConfigurationChanged();
    108             }
    109         }
    110     };
    111 
    112     static int resolveDialogTheme(Context context) {
    113         TypedValue outValue = new TypedValue();
    114         context.getTheme().resolveAttribute(com.android.internal.R.attr.searchDialogTheme,
    115                 outValue, true);
    116         return outValue.resourceId;
    117     }
    118 
    119     /**
    120      * Constructor - fires it up and makes it look like the search UI.
    121      *
    122      * @param context Application Context we can use for system acess
    123      */
    124     public SearchDialog(Context context, SearchManager searchManager) {
    125         super(context, resolveDialogTheme(context));
    126 
    127         // Save voice intent for later queries/launching
    128         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
    129         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    130         mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
    131                 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
    132 
    133         mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    134         mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    135     }
    136 
    137     /**
    138      * Create the search dialog and any resources that are used for the
    139      * entire lifetime of the dialog.
    140      */
    141     @Override
    142     protected void onCreate(Bundle savedInstanceState) {
    143         super.onCreate(savedInstanceState);
    144 
    145         Window theWindow = getWindow();
    146         WindowManager.LayoutParams lp = theWindow.getAttributes();
    147         lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
    148         // taking up the whole window (even when transparent) is less than ideal,
    149         // but necessary to show the popup window until the window manager supports
    150         // having windows anchored by their parent but not clipped by them.
    151         lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
    152         lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
    153         lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
    154         theWindow.setAttributes(lp);
    155 
    156         // Touching outside of the search dialog will dismiss it
    157         setCanceledOnTouchOutside(true);
    158     }
    159 
    160     /**
    161      * We recreate the dialog view each time it becomes visible so as to limit
    162      * the scope of any problems with the contained resources.
    163      */
    164     private void createContentView() {
    165         setContentView(com.android.internal.R.layout.search_bar);
    166 
    167         // get the view elements for local access
    168         mSearchView = findViewById(com.android.internal.R.id.search_view);
    169         mSearchView.setIconified(false);
    170         mSearchView.setOnCloseListener(mOnCloseListener);
    171         mSearchView.setOnQueryTextListener(mOnQueryChangeListener);
    172         mSearchView.setOnSuggestionListener(mOnSuggestionSelectionListener);
    173         mSearchView.onActionViewExpanded();
    174 
    175         mCloseSearch = findViewById(com.android.internal.R.id.closeButton);
    176         mCloseSearch.setOnClickListener(new View.OnClickListener() {
    177             @Override
    178             public void onClick(View v) {
    179                 dismiss();
    180             }
    181         });
    182 
    183         // TODO: Move the badge logic to SearchView or move the badge to search_bar.xml
    184         mBadgeLabel = (TextView) mSearchView.findViewById(com.android.internal.R.id.search_badge);
    185         mSearchAutoComplete = (AutoCompleteTextView)
    186                 mSearchView.findViewById(com.android.internal.R.id.search_src_text);
    187         mAppIcon = findViewById(com.android.internal.R.id.search_app_icon);
    188         mSearchPlate = mSearchView.findViewById(com.android.internal.R.id.search_plate);
    189         mWorkingSpinner = getContext().getDrawable(com.android.internal.R.drawable.search_spinner);
    190         // TODO: Restore the spinner for slow suggestion lookups
    191         // mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
    192         //        null, null, mWorkingSpinner, null);
    193         setWorking(false);
    194 
    195         // pre-hide all the extraneous elements
    196         mBadgeLabel.setVisibility(View.GONE);
    197 
    198         // Additional adjustments to make Dialog work for Search
    199         mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
    200     }
    201 
    202     /**
    203      * Set up the search dialog
    204      *
    205      * @return true if search dialog launched, false if not
    206      */
    207     public boolean show(String initialQuery, boolean selectInitialQuery,
    208             ComponentName componentName, Bundle appSearchData) {
    209         boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData);
    210         if (success) {
    211             // Display the drop down as soon as possible instead of waiting for the rest of the
    212             // pending UI stuff to get done, so that things appear faster to the user.
    213             mSearchAutoComplete.showDropDownAfterLayout();
    214         }
    215         return success;
    216     }
    217 
    218     /**
    219      * Does the rest of the work required to show the search dialog. Called by
    220      * {@link #show(String, boolean, ComponentName, Bundle)} and
    221      *
    222      * @return true if search dialog showed, false if not
    223      */
    224     private boolean doShow(String initialQuery, boolean selectInitialQuery,
    225             ComponentName componentName, Bundle appSearchData) {
    226         // set up the searchable and show the dialog
    227         if (!show(componentName, appSearchData)) {
    228             return false;
    229         }
    230 
    231         // finally, load the user's initial text (which may trigger suggestions)
    232         setUserQuery(initialQuery);
    233         if (selectInitialQuery) {
    234             mSearchAutoComplete.selectAll();
    235         }
    236 
    237         return true;
    238     }
    239 
    240     /**
    241      * Sets up the search dialog and shows it.
    242      *
    243      * @return <code>true</code> if search dialog launched
    244      */
    245     private boolean show(ComponentName componentName, Bundle appSearchData) {
    246 
    247         if (DBG) {
    248             Log.d(LOG_TAG, "show(" + componentName + ", "
    249                     + appSearchData + ")");
    250         }
    251 
    252         SearchManager searchManager = (SearchManager)
    253                 mContext.getSystemService(Context.SEARCH_SERVICE);
    254         // Try to get the searchable info for the provided component.
    255         mSearchable = searchManager.getSearchableInfo(componentName);
    256 
    257         if (mSearchable == null) {
    258             return false;
    259         }
    260 
    261         mLaunchComponent = componentName;
    262         mAppSearchData = appSearchData;
    263         mActivityContext = mSearchable.getActivityContext(getContext());
    264 
    265         // show the dialog. this will call onStart().
    266         if (!isShowing()) {
    267             // Recreate the search bar view every time the dialog is shown, to get rid
    268             // of any bad state in the AutoCompleteTextView etc
    269             createContentView();
    270             mSearchView.setSearchableInfo(mSearchable);
    271             mSearchView.setAppSearchData(mAppSearchData);
    272 
    273             show();
    274         }
    275         updateUI();
    276 
    277         return true;
    278     }
    279 
    280     @Override
    281     public void onStart() {
    282         super.onStart();
    283 
    284         // Register a listener for configuration change events.
    285         IntentFilter filter = new IntentFilter();
    286         filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
    287         getContext().registerReceiver(mConfChangeListener, filter);
    288     }
    289 
    290     /**
    291      * The search dialog is being dismissed, so handle all of the local shutdown operations.
    292      *
    293      * This function is designed to be idempotent so that dismiss() can be safely called at any time
    294      * (even if already closed) and more likely to really dump any memory.  No leaks!
    295      */
    296     @Override
    297     public void onStop() {
    298         super.onStop();
    299 
    300         getContext().unregisterReceiver(mConfChangeListener);
    301 
    302         // dump extra memory we're hanging on to
    303         mLaunchComponent = null;
    304         mAppSearchData = null;
    305         mSearchable = null;
    306         mUserQuery = null;
    307     }
    308 
    309     /**
    310      * Sets the search dialog to the 'working' state, which shows a working spinner in the
    311      * right hand size of the text field.
    312      *
    313      * @param working true to show spinner, false to hide spinner
    314      */
    315     public void setWorking(boolean working) {
    316         mWorkingSpinner.setAlpha(working ? 255 : 0);
    317         mWorkingSpinner.setVisible(working, false);
    318         mWorkingSpinner.invalidateSelf();
    319     }
    320 
    321     /**
    322      * Save the minimal set of data necessary to recreate the search
    323      *
    324      * @return A bundle with the state of the dialog, or {@code null} if the search
    325      *         dialog is not showing.
    326      */
    327     @Override
    328     public Bundle onSaveInstanceState() {
    329         if (!isShowing()) return null;
    330 
    331         Bundle bundle = new Bundle();
    332 
    333         // setup info so I can recreate this particular search
    334         bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
    335         bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
    336         bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
    337 
    338         return bundle;
    339     }
    340 
    341     /**
    342      * Restore the state of the dialog from a previously saved bundle.
    343      *
    344      * @param savedInstanceState The state of the dialog previously saved by
    345      *     {@link #onSaveInstanceState()}.
    346      */
    347     @Override
    348     public void onRestoreInstanceState(Bundle savedInstanceState) {
    349         if (savedInstanceState == null) return;
    350 
    351         ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
    352         Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
    353         String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
    354 
    355         // show the dialog.
    356         if (!doShow(userQuery, false, launchComponent, appSearchData)) {
    357             // for some reason, we couldn't re-instantiate
    358             return;
    359         }
    360     }
    361 
    362     /**
    363      * Called after resources have changed, e.g. after screen rotation or locale change.
    364      */
    365     public void onConfigurationChanged() {
    366         if (mSearchable != null && isShowing()) {
    367             // Redraw (resources may have changed)
    368             updateSearchAppIcon();
    369             updateSearchBadge();
    370             if (isLandscapeMode(getContext())) {
    371                 mSearchAutoComplete.ensureImeVisible(true);
    372             }
    373         }
    374     }
    375 
    376     static boolean isLandscapeMode(Context context) {
    377         return context.getResources().getConfiguration().orientation
    378                 == Configuration.ORIENTATION_LANDSCAPE;
    379     }
    380 
    381     /**
    382      * Update the UI according to the info in the current value of {@link #mSearchable}.
    383      */
    384     private void updateUI() {
    385         if (mSearchable != null) {
    386             mDecor.setVisibility(View.VISIBLE);
    387             updateSearchAutoComplete();
    388             updateSearchAppIcon();
    389             updateSearchBadge();
    390 
    391             // In order to properly configure the input method (if one is being used), we
    392             // need to let it know if we'll be providing suggestions.  Although it would be
    393             // difficult/expensive to know if every last detail has been configured properly, we
    394             // can at least see if a suggestions provider has been configured, and use that
    395             // as our trigger.
    396             int inputType = mSearchable.getInputType();
    397             // We only touch this if the input type is set up for text (which it almost certainly
    398             // should be, in the case of search!)
    399             if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
    400                 // The existence of a suggestions authority is the proxy for "suggestions
    401                 // are available here"
    402                 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
    403                 if (mSearchable.getSuggestAuthority() != null) {
    404                     inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
    405                 }
    406             }
    407             mSearchAutoComplete.setInputType(inputType);
    408             mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
    409             mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
    410 
    411             // If the search dialog is going to show a voice search button, then don't let
    412             // the soft keyboard display a microphone button if it would have otherwise.
    413             if (mSearchable.getVoiceSearchEnabled()) {
    414                 mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
    415             } else {
    416                 mSearchAutoComplete.setPrivateImeOptions(null);
    417             }
    418         }
    419     }
    420 
    421     /**
    422      * Updates the auto-complete text view.
    423      */
    424     private void updateSearchAutoComplete() {
    425         // we dismiss the entire dialog instead
    426         mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
    427         mSearchAutoComplete.setForceIgnoreOutsideTouch(false);
    428     }
    429 
    430     private void updateSearchAppIcon() {
    431         PackageManager pm = getContext().getPackageManager();
    432         Drawable icon;
    433         try {
    434             ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
    435             icon = pm.getApplicationIcon(info.applicationInfo);
    436             if (DBG)
    437                 Log.d(LOG_TAG, "Using app-specific icon");
    438         } catch (NameNotFoundException e) {
    439             icon = pm.getDefaultActivityIcon();
    440             Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
    441         }
    442         mAppIcon.setImageDrawable(icon);
    443         mAppIcon.setVisibility(View.VISIBLE);
    444         mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, mSearchPlate.getPaddingTop(), mSearchPlate.getPaddingRight(), mSearchPlate.getPaddingBottom());
    445     }
    446 
    447     /**
    448      * Setup the search "Badge" if requested by mode flags.
    449      */
    450     private void updateSearchBadge() {
    451         // assume both hidden
    452         int visibility = View.GONE;
    453         Drawable icon = null;
    454         CharSequence text = null;
    455 
    456         // optionally show one or the other.
    457         if (mSearchable.useBadgeIcon()) {
    458             icon = mActivityContext.getDrawable(mSearchable.getIconId());
    459             visibility = View.VISIBLE;
    460             if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
    461         } else if (mSearchable.useBadgeLabel()) {
    462             text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
    463             visibility = View.VISIBLE;
    464             if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
    465         }
    466 
    467         mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
    468         mBadgeLabel.setText(text);
    469         mBadgeLabel.setVisibility(visibility);
    470     }
    471 
    472     /*
    473      * Listeners of various types
    474      */
    475 
    476     /**
    477      * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
    478      * touch is outside the window. But the window includes space for the drop-down,
    479      * so we also cancel on taps outside the search bar when the drop-down is not showing.
    480      */
    481     @Override
    482     public boolean onTouchEvent(MotionEvent event) {
    483         // cancel if the drop-down is not showing and the touch event was outside the search plate
    484         if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
    485             if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
    486             cancel();
    487             return true;
    488         }
    489         // Let Dialog handle events outside the window while the pop-up is showing.
    490         return super.onTouchEvent(event);
    491     }
    492 
    493     private boolean isOutOfBounds(View v, MotionEvent event) {
    494         final int x = (int) event.getX();
    495         final int y = (int) event.getY();
    496         final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
    497         return (x < -slop) || (y < -slop)
    498                 || (x > (v.getWidth()+slop))
    499                 || (y > (v.getHeight()+slop));
    500     }
    501 
    502     @Override
    503     public void hide() {
    504         if (!isShowing()) return;
    505 
    506         // We made sure the IME was displayed, so also make sure it is closed
    507         // when we go away.
    508         InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
    509         if (imm != null) {
    510             imm.hideSoftInputFromWindow(
    511                     getWindow().getDecorView().getWindowToken(), 0);
    512         }
    513 
    514         super.hide();
    515     }
    516 
    517     /**
    518      * Launch a search for the text in the query text field.
    519      */
    520     public void launchQuerySearch() {
    521         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
    522     }
    523 
    524     /**
    525      * Launch a search for the text in the query text field.
    526      *
    527      * @param actionKey The key code of the action key that was pressed,
    528      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
    529      * @param actionMsg The message for the action key that was pressed,
    530      *        or <code>null</code> if none.
    531      */
    532     protected void launchQuerySearch(int actionKey, String actionMsg) {
    533         String query = mSearchAutoComplete.getText().toString();
    534         String action = Intent.ACTION_SEARCH;
    535         Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
    536         launchIntent(intent);
    537     }
    538 
    539     /**
    540      * Launches an intent, including any special intent handling.
    541      */
    542     private void launchIntent(Intent intent) {
    543         if (intent == null) {
    544             return;
    545         }
    546         Log.d(LOG_TAG, "launching " + intent);
    547         try {
    548             // If the intent was created from a suggestion, it will always have an explicit
    549             // component here.
    550             getContext().startActivity(intent);
    551             // If the search switches to a different activity,
    552             // SearchDialogWrapper#performActivityResuming
    553             // will handle hiding the dialog when the next activity starts, but for
    554             // real in-app search, we still need to dismiss the dialog.
    555             dismiss();
    556         } catch (RuntimeException ex) {
    557             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
    558         }
    559     }
    560 
    561     /**
    562      * Sets the list item selection in the AutoCompleteTextView's ListView.
    563      */
    564     public void setListSelection(int index) {
    565         mSearchAutoComplete.setListSelection(index);
    566     }
    567 
    568     /**
    569      * Constructs an intent from the given information and the search dialog state.
    570      *
    571      * @param action Intent action.
    572      * @param data Intent data, or <code>null</code>.
    573      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
    574      * @param query Intent query, or <code>null</code>.
    575      * @param actionKey The key code of the action key that was pressed,
    576      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
    577      * @param actionMsg The message for the action key that was pressed,
    578      *        or <code>null</code> if none.
    579      * @param mode The search mode, one of the acceptable values for
    580      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
    581      * @return The intent.
    582      */
    583     private Intent createIntent(String action, Uri data, String extraData, String query,
    584             int actionKey, String actionMsg) {
    585         // Now build the Intent
    586         Intent intent = new Intent(action);
    587         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    588         // We need CLEAR_TOP to avoid reusing an old task that has other activities
    589         // on top of the one we want. We don't want to do this in in-app search though,
    590         // as it can be destructive to the activity stack.
    591         if (data != null) {
    592             intent.setData(data);
    593         }
    594         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
    595         if (query != null) {
    596             intent.putExtra(SearchManager.QUERY, query);
    597         }
    598         if (extraData != null) {
    599             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
    600         }
    601         if (mAppSearchData != null) {
    602             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
    603         }
    604         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
    605             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
    606             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
    607         }
    608         intent.setComponent(mSearchable.getSearchActivity());
    609         return intent;
    610     }
    611 
    612     /**
    613      * The root element in the search bar layout. This is a custom view just to override
    614      * the handling of the back button.
    615      */
    616     public static class SearchBar extends LinearLayout {
    617 
    618         public SearchBar(Context context, AttributeSet attrs) {
    619             super(context, attrs);
    620         }
    621 
    622         public SearchBar(Context context) {
    623             super(context);
    624         }
    625 
    626         @Override
    627         public ActionMode startActionModeForChild(
    628                 View child, ActionMode.Callback callback, int type) {
    629             // Disable Primary Action Modes in the SearchBar, as they overlap.
    630             if (type != ActionMode.TYPE_PRIMARY) {
    631                 return super.startActionModeForChild(child, callback, type);
    632             }
    633             return null;
    634         }
    635     }
    636 
    637     private boolean isEmpty(AutoCompleteTextView actv) {
    638         return TextUtils.getTrimmedLength(actv.getText()) == 0;
    639     }
    640 
    641     @Override
    642     public void onBackPressed() {
    643         // If the input method is covering the search dialog completely,
    644         // e.g. in landscape mode with no hard keyboard, dismiss just the input method
    645         InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
    646         if (imm != null && imm.isFullscreenMode() &&
    647                 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) {
    648             return;
    649         }
    650         // Close search dialog
    651         cancel();
    652     }
    653 
    654     private boolean onClosePressed() {
    655         // Dismiss the dialog if close button is pressed when there's no query text
    656         if (isEmpty(mSearchAutoComplete)) {
    657             dismiss();
    658             return true;
    659         }
    660 
    661         return false;
    662     }
    663 
    664     private final SearchView.OnCloseListener mOnCloseListener = new SearchView.OnCloseListener() {
    665 
    666         public boolean onClose() {
    667             return onClosePressed();
    668         }
    669     };
    670 
    671     private final SearchView.OnQueryTextListener mOnQueryChangeListener =
    672             new SearchView.OnQueryTextListener() {
    673 
    674         public boolean onQueryTextSubmit(String query) {
    675             dismiss();
    676             return false;
    677         }
    678 
    679         public boolean onQueryTextChange(String newText) {
    680             return false;
    681         }
    682     };
    683 
    684     private final SearchView.OnSuggestionListener mOnSuggestionSelectionListener =
    685             new SearchView.OnSuggestionListener() {
    686 
    687         public boolean onSuggestionSelect(int position) {
    688             return false;
    689         }
    690 
    691         public boolean onSuggestionClick(int position) {
    692             dismiss();
    693             return false;
    694         }
    695     };
    696 
    697     /**
    698      * Sets the text in the query box, updating the suggestions.
    699      */
    700     private void setUserQuery(String query) {
    701         if (query == null) {
    702             query = "";
    703         }
    704         mUserQuery = query;
    705         mSearchAutoComplete.setText(query);
    706         mSearchAutoComplete.setSelection(query.length());
    707     }
    708 }
    709