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