• 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 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      */
SearchDialog(Context context, SearchManager searchManager)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
onCreate(Bundle savedInstanceState)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      */
createContentView()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      */
show(String initialQuery, boolean selectInitialQuery, ComponentName componentName, Bundle appSearchData)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      */
doShow(String initialQuery, boolean selectInitialQuery, ComponentName componentName, Bundle appSearchData)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      */
show(ComponentName componentName, Bundle appSearchData)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
onStart()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
onStop()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      */
setWorking(boolean working)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      */
closeSuggestionsAdapter()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
onSaveInstanceState()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
onRestoreInstanceState(Bundle savedInstanceState)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      */
onConfigurationChanged()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 
isLandscapeMode(Context context)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      */
updateUI()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      */
updateSearchAutoComplete()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 
updateSearchButton()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 
updateSearchAppIcon()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      */
updateSearchBadge()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      */
updateQueryHint()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      */
updateVoiceButton(boolean empty)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. */
onDataSetChanged()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      */
isBrowserSearch()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
onTouchEvent(MotionEvent event)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 
isOutOfBounds(View v, MotionEvent event)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
onKeyDown(int keyCode, KeyEvent event)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      */
updateWidgetState()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      */
createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable)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      */
createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable)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      */
fixUrl(String inUrl)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
hide()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      */
onSuggestionsKey(View v, int keyCode, KeyEvent event)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      */
launchQuerySearch()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      */
launchQuerySearch(int actionKey, String actionMsg)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      */
launchSuggestion(int position)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      */
launchSuggestion(int position, int actionKey, String actionMsg)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      */
launchIntent(Intent intent)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      */
setBrowserApplicationId(Intent intent)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      */
setListSelection(int index)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      */
createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg)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      */
createIntent(String action, Uri data, String extraData, String query, String componentName, int actionKey, String actionMsg)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      */
getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey)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 
SearchBar(Context context, AttributeSet attrs)1312         public SearchBar(Context context, AttributeSet attrs) {
1313             super(context, attrs);
1314         }
1315 
SearchBar(Context context)1316         public SearchBar(Context context) {
1317             super(context);
1318         }
1319 
setSearchDialog(SearchDialog searchDialog)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
dispatchKeyEventPreIme(KeyEvent event)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 
SearchAutoComplete(Context context)1356         public SearchAutoComplete(Context context) {
1357             super(context);
1358             mThreshold = getThreshold();
1359         }
1360 
SearchAutoComplete(Context context, AttributeSet attrs)1361         public SearchAutoComplete(Context context, AttributeSet attrs) {
1362             super(context, attrs);
1363             mThreshold = getThreshold();
1364         }
1365 
SearchAutoComplete(Context context, AttributeSet attrs, int defStyle)1366         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1367             super(context, attrs, defStyle);
1368             mThreshold = getThreshold();
1369         }
1370 
1371         @Override
setThreshold(int threshold)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          */
isEmpty()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
replaceText(CharSequence text)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
performCompletion()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
onWindowFocusChanged(boolean hasWindowFocus)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
enoughToFilter()1425         public boolean enoughToFilter() {
1426             return mThreshold <= 0 || super.enoughToFilter();
1427         }
1428 
1429     }
1430 
1431     @Override
onBackPressed()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      */
onItemClick(AdapterView<?> parent, View view, int position, long id)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      */
onItemSelected(AdapterView<?> parent, View view, int position, long id)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       */
onNothingSelected(AdapterView<?> parent)1468      public void onNothingSelected(AdapterView<?> parent) {
1469          if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
1470      }
1471 
1472      /**
1473       * Query rewriting.
1474       */
1475 
rewriteQueryFromSuggestion(int position)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       */
restoreUserQuery()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       */
setQuery(CharSequence query)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       */
setUserQuery(String query)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());
dbgLogTiming(final String caller)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