• 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 import static android.app.SuggestionsAdapter.getColumnString;
20 
21 import android.content.ActivityNotFoundException;
22 import android.content.ComponentName;
23 import android.content.ContentResolver;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ActivityInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.content.res.Resources;
32 import android.database.Cursor;
33 import android.graphics.drawable.Drawable;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.os.IBinder;
37 import android.os.RemoteException;
38 import android.os.SystemClock;
39 import android.provider.Browser;
40 import android.server.search.SearchableInfo;
41 import android.speech.RecognizerIntent;
42 import android.text.Editable;
43 import android.text.InputType;
44 import android.text.TextUtils;
45 import android.text.TextWatcher;
46 import android.text.util.Regex;
47 import android.util.AndroidRuntimeException;
48 import android.util.AttributeSet;
49 import android.util.Log;
50 import android.view.ContextThemeWrapper;
51 import android.view.Gravity;
52 import android.view.KeyEvent;
53 import android.view.MotionEvent;
54 import android.view.View;
55 import android.view.ViewConfiguration;
56 import android.view.ViewGroup;
57 import android.view.Window;
58 import android.view.WindowManager;
59 import android.view.inputmethod.EditorInfo;
60 import android.view.inputmethod.InputMethodManager;
61 import android.widget.AdapterView;
62 import android.widget.AutoCompleteTextView;
63 import android.widget.Button;
64 import android.widget.ImageButton;
65 import android.widget.ImageView;
66 import android.widget.LinearLayout;
67 import android.widget.ListView;
68 import android.widget.TextView;
69 import android.widget.AdapterView.OnItemClickListener;
70 import android.widget.AdapterView.OnItemSelectedListener;
71 
72 import java.util.ArrayList;
73 import java.util.WeakHashMap;
74 import java.util.concurrent.atomic.AtomicLong;
75 
76 /**
77  * System search dialog. This is controlled by the
78  * SearchManagerService and runs in the system process.
79  *
80  * @hide
81  */
82 public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
83 
84     // Debugging support
85     private static final boolean DBG = false;
86     private static final String LOG_TAG = "SearchDialog";
87     private static final boolean DBG_LOG_TIMING = false;
88 
89     private static final String INSTANCE_KEY_COMPONENT = "comp";
90     private static final String INSTANCE_KEY_APPDATA = "data";
91     private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
92     private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp";
93     private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
94     private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev";
95     private static final String INSTANCE_KEY_USER_QUERY = "uQry";
96 
97     // The extra key used in an intent to the speech recognizer for in-app voice search.
98     private static final String EXTRA_CALLING_PACKAGE = "calling_package";
99 
100     // The string used for privateImeOptions to identify to the IME that it should not show
101     // a microphone button since one already exists in the search dialog.
102     private static final String IME_OPTION_NO_MICROPHONE = "nm";
103 
104     private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
105     private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
106 
107     // views & widgets
108     private TextView mBadgeLabel;
109     private ImageView mAppIcon;
110     private SearchAutoComplete mSearchAutoComplete;
111     private Button mGoButton;
112     private ImageButton mVoiceButton;
113     private View mSearchPlate;
114     private Drawable mWorkingSpinner;
115 
116     // interaction with searchable application
117     private SearchableInfo mSearchable;
118     private ComponentName mLaunchComponent;
119     private Bundle mAppSearchData;
120     private boolean mGlobalSearchMode;
121     private Context mActivityContext;
122 
123     // Values we store to allow user to toggle between in-app search and global search.
124     private ComponentName mStoredComponentName;
125     private Bundle mStoredAppSearchData;
126 
127     // stack of previous searchables, to support the BACK key after
128     // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.
129     // The top of the stack (= previous searchable) is the last element of the list,
130     // since adding and removing is efficient at the end of an ArrayList.
131     private ArrayList<ComponentName> mPreviousComponents;
132 
133     // For voice searching
134     private final Intent mVoiceWebSearchIntent;
135     private final Intent mVoiceAppSearchIntent;
136 
137     // support for AutoCompleteTextView suggestions display
138     private SuggestionsAdapter mSuggestionsAdapter;
139 
140     // Whether to rewrite queries when selecting suggestions
141     private static final boolean REWRITE_QUERIES = true;
142 
143     // The query entered by the user. This is not changed when selecting a suggestion
144     // that modifies the contents of the text field. But if the user then edits
145     // the suggestion, the resulting string is saved.
146     private String mUserQuery;
147 
148     // A weak map of drawables we've gotten from other packages, so we don't load them
149     // more than once.
150     private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
151             new WeakHashMap<String, Drawable.ConstantState>();
152 
153     // Last known IME options value for the search edit text.
154     private int mSearchAutoCompleteImeOptions;
155 
156     /**
157      * Constructor - fires it up and makes it look like the search UI.
158      *
159      * @param context Application Context we can use for system acess
160      */
SearchDialog(Context context)161     public SearchDialog(Context context) {
162         super(context, com.android.internal.R.style.Theme_GlobalSearchBar);
163 
164         // Save voice intent for later queries/launching
165         mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
166         mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
167         mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
168                 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
169 
170         mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
171         mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
172     }
173 
174     /**
175      * Create the search dialog and any resources that are used for the
176      * entire lifetime of the dialog.
177      */
178     @Override
onCreate(Bundle savedInstanceState)179     protected void onCreate(Bundle savedInstanceState) {
180         super.onCreate(savedInstanceState);
181 
182         Window theWindow = getWindow();
183         WindowManager.LayoutParams lp = theWindow.getAttributes();
184         lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR;
185         lp.width = ViewGroup.LayoutParams.FILL_PARENT;
186         // taking up the whole window (even when transparent) is less than ideal,
187         // but necessary to show the popup window until the window manager supports
188         // having windows anchored by their parent but not clipped by them.
189         lp.height = ViewGroup.LayoutParams.FILL_PARENT;
190         lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
191         lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
192         theWindow.setAttributes(lp);
193 
194         // Touching outside of the search dialog will dismiss it
195         setCanceledOnTouchOutside(true);
196     }
197 
198     /**
199      * We recreate the dialog view each time it becomes visible so as to limit
200      * the scope of any problems with the contained resources.
201      */
createContentView()202     private void createContentView() {
203         setContentView(com.android.internal.R.layout.search_bar);
204 
205         // get the view elements for local access
206         SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar);
207         searchBar.setSearchDialog(this);
208 
209         mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
210         mSearchAutoComplete = (SearchAutoComplete)
211                 findViewById(com.android.internal.R.id.search_src_text);
212         mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
213         mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
214         mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
215         mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
216         mWorkingSpinner = getContext().getResources().
217                 getDrawable(com.android.internal.R.drawable.search_spinner);
218         mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
219                 null, null, mWorkingSpinner, null);
220         setWorking(false);
221 
222         // attach listeners
223         mSearchAutoComplete.addTextChangedListener(mTextWatcher);
224         mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
225         mSearchAutoComplete.setOnItemClickListener(this);
226         mSearchAutoComplete.setOnItemSelectedListener(this);
227         mGoButton.setOnClickListener(mGoButtonClickListener);
228         mGoButton.setOnKeyListener(mButtonsKeyListener);
229         mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
230         mVoiceButton.setOnKeyListener(mButtonsKeyListener);
231 
232         // pre-hide all the extraneous elements
233         mBadgeLabel.setVisibility(View.GONE);
234 
235         // Additional adjustments to make Dialog work for Search
236         mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
237     }
238 
239     /**
240      * Set up the search dialog
241      *
242      * @return true if search dialog launched, false if not
243      */
show(String initialQuery, boolean selectInitialQuery, ComponentName componentName, Bundle appSearchData, boolean globalSearch)244     public boolean show(String initialQuery, boolean selectInitialQuery,
245             ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
246 
247         // Reset any stored values from last time dialog was shown.
248         mStoredComponentName = null;
249         mStoredAppSearchData = null;
250 
251         boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData,
252                 globalSearch);
253         if (success) {
254             // Display the drop down as soon as possible instead of waiting for the rest of the
255             // pending UI stuff to get done, so that things appear faster to the user.
256             mSearchAutoComplete.showDropDownAfterLayout();
257         }
258         return success;
259     }
260 
isInRealAppSearch()261     private boolean isInRealAppSearch() {
262         return !mGlobalSearchMode
263                 && (mPreviousComponents == null || mPreviousComponents.isEmpty());
264     }
265 
266     /**
267      * Called in response to a press of the hard search button in
268      * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app
269      * search and global search when relevant.
270      *
271      * If pressed within an in-app search context, this switches the search dialog out to
272      * global search. If pressed within a global search context that was originally an in-app
273      * search context, this switches back to the in-app search context. If pressed within a
274      * global search context that has no original in-app search context (e.g., global search
275      * from Home), this does nothing.
276      *
277      * @return false if we wanted to toggle context but could not do so successfully, true
278      * in all other cases
279      */
toggleGlobalSearch()280     private boolean toggleGlobalSearch() {
281         String currentSearchText = mSearchAutoComplete.getText().toString();
282         if (!mGlobalSearchMode) {
283             mStoredComponentName = mLaunchComponent;
284             mStoredAppSearchData = mAppSearchData;
285 
286             // If this is the browser, we have a special case to not show the icon to the left
287             // of the text field, for extra space for url entry (this should be reconciled in
288             // Eclair). So special case a second tap of the search button to remove any
289             // already-entered text so that we can be sure to show the "Quick Search Box" hint
290             // text to still make it clear to the user that we've jumped out to global search.
291             //
292             // TODO: When the browser icon issue is reconciled in Eclair, remove this special case.
293             if (isBrowserSearch()) currentSearchText = "";
294 
295             return doShow(currentSearchText, false, null, mAppSearchData, true);
296         } else {
297             if (mStoredComponentName != null) {
298                 // This means we should toggle *back* to an in-app search context from
299                 // global search.
300                 return doShow(currentSearchText, false, mStoredComponentName,
301                         mStoredAppSearchData, false);
302             } else {
303                 return true;
304             }
305         }
306     }
307 
308     /**
309      * Does the rest of the work required to show the search dialog. Called by both
310      * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and
311      * {@link #toggleGlobalSearch()}.
312      *
313      * @return true if search dialog showed, false if not
314      */
doShow(String initialQuery, boolean selectInitialQuery, ComponentName componentName, Bundle appSearchData, boolean globalSearch)315     private boolean doShow(String initialQuery, boolean selectInitialQuery,
316             ComponentName componentName, Bundle appSearchData,
317             boolean globalSearch) {
318         // set up the searchable and show the dialog
319         if (!show(componentName, appSearchData, globalSearch)) {
320             return false;
321         }
322 
323         // finally, load the user's initial text (which may trigger suggestions)
324         setUserQuery(initialQuery);
325         if (selectInitialQuery) {
326             mSearchAutoComplete.selectAll();
327         }
328 
329         return true;
330     }
331 
332     /**
333      * Sets up the search dialog and shows it.
334      *
335      * @return <code>true</code> if search dialog launched
336      */
show(ComponentName componentName, Bundle appSearchData, boolean globalSearch)337     private boolean show(ComponentName componentName, Bundle appSearchData,
338             boolean globalSearch) {
339 
340         if (DBG) {
341             Log.d(LOG_TAG, "show(" + componentName + ", "
342                     + appSearchData + ", " + globalSearch + ")");
343         }
344 
345         SearchManager searchManager = (SearchManager)
346                 mContext.getSystemService(Context.SEARCH_SERVICE);
347         // Try to get the searchable info for the provided component (or for global search,
348         // if globalSearch == true).
349         mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
350 
351         // If we got back nothing, and it wasn't a request for global search, then try again
352         // for global search, as we'll try to launch that in lieu of any component-specific search.
353         if (!globalSearch && mSearchable == null) {
354             globalSearch = true;
355             mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
356         }
357 
358         // If there's not even a searchable info available for global search, then really give up.
359         if (mSearchable == null) {
360             Log.w(LOG_TAG, "No global search provider.");
361             return false;
362         }
363 
364         mLaunchComponent = componentName;
365         mAppSearchData = appSearchData;
366         // Using globalSearch here is just an optimization, just calling
367         // isDefaultSearchable() should always give the same result.
368         mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable);
369         mActivityContext = mSearchable.getActivityContext(getContext());
370 
371         // show the dialog. this will call onStart().
372         if (!isShowing()) {
373             // Recreate the search bar view every time the dialog is shown, to get rid
374             // of any bad state in the AutoCompleteTextView etc
375             createContentView();
376 
377             // The Dialog uses a ContextThemeWrapper for the context; use this to change the
378             // theme out from underneath us, between the global search theme and the in-app
379             // search theme. They are identical except that the global search theme does not
380             // dim the background of the window (because global search is full screen so it's
381             // not needed and this should save a little bit of time on global search invocation).
382             Object context = getContext();
383             if (context instanceof ContextThemeWrapper) {
384                 ContextThemeWrapper wrapper = (ContextThemeWrapper) context;
385                 if (globalSearch) {
386                     wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar);
387                 } else {
388                     wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar);
389                 }
390             }
391             show();
392         }
393         updateUI();
394 
395         return true;
396     }
397 
398     /**
399      * The search dialog is being dismissed, so handle all of the local shutdown operations.
400      *
401      * This function is designed to be idempotent so that dismiss() can be safely called at any time
402      * (even if already closed) and more likely to really dump any memory.  No leaks!
403      */
404     @Override
onStop()405     public void onStop() {
406         super.onStop();
407 
408         closeSuggestionsAdapter();
409 
410         // dump extra memory we're hanging on to
411         mLaunchComponent = null;
412         mAppSearchData = null;
413         mSearchable = null;
414         mActivityContext = null;
415         mUserQuery = null;
416         mPreviousComponents = null;
417     }
418 
419     /**
420      * Sets the search dialog to the 'working' state, which shows a working spinner in the
421      * right hand size of the text field.
422      *
423      * @param working true to show spinner, false to hide spinner
424      */
setWorking(boolean working)425     public void setWorking(boolean working) {
426         mWorkingSpinner.setAlpha(working ? 255 : 0);
427         mWorkingSpinner.setVisible(working, false);
428         mWorkingSpinner.invalidateSelf();
429     }
430 
431     /**
432      * Closes and gets rid of the suggestions adapter.
433      */
closeSuggestionsAdapter()434     private void closeSuggestionsAdapter() {
435         // remove the adapter from the autocomplete first, to avoid any updates
436         // when we drop the cursor
437         mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
438         // close any leftover cursor
439         if (mSuggestionsAdapter != null) {
440             mSuggestionsAdapter.close();
441         }
442         mSuggestionsAdapter = null;
443     }
444 
445     /**
446      * Save the minimal set of data necessary to recreate the search
447      *
448      * @return A bundle with the state of the dialog, or {@code null} if the search
449      *         dialog is not showing.
450      */
451     @Override
onSaveInstanceState()452     public Bundle onSaveInstanceState() {
453         if (!isShowing()) return null;
454 
455         Bundle bundle = new Bundle();
456 
457         // setup info so I can recreate this particular search
458         bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
459         bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
460         bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
461         bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName);
462         bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData);
463         bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents);
464         bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
465 
466         return bundle;
467     }
468 
469     /**
470      * Restore the state of the dialog from a previously saved bundle.
471      *
472      * TODO: go through this and make sure that it saves everything that is saved
473      *
474      * @param savedInstanceState The state of the dialog previously saved by
475      *     {@link #onSaveInstanceState()}.
476      */
477     @Override
onRestoreInstanceState(Bundle savedInstanceState)478     public void onRestoreInstanceState(Bundle savedInstanceState) {
479         if (savedInstanceState == null) return;
480 
481         ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
482         Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
483         boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
484         ComponentName storedComponentName =
485                 savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT);
486         Bundle storedAppSearchData =
487                 savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA);
488         ArrayList<ComponentName> previousComponents =
489                 savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS);
490         String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
491 
492         // Set stored state
493         mStoredComponentName = storedComponentName;
494         mStoredAppSearchData = storedAppSearchData;
495         mPreviousComponents = previousComponents;
496 
497         // show the dialog.
498         if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) {
499             // for some reason, we couldn't re-instantiate
500             return;
501         }
502     }
503 
504     /**
505      * Called after resources have changed, e.g. after screen rotation or locale change.
506      */
onConfigurationChanged()507     public void onConfigurationChanged() {
508         if (isShowing()) {
509             // Redraw (resources may have changed)
510             updateSearchButton();
511             updateSearchAppIcon();
512             updateSearchBadge();
513             updateQueryHint();
514             mSearchAutoComplete.showDropDownAfterLayout();
515         }
516     }
517 
518     /**
519      * Update the UI according to the info in the current value of {@link #mSearchable}.
520      */
updateUI()521     private void updateUI() {
522         if (mSearchable != null) {
523             mDecor.setVisibility(View.VISIBLE);
524             updateSearchAutoComplete();
525             updateSearchButton();
526             updateSearchAppIcon();
527             updateSearchBadge();
528             updateQueryHint();
529             updateVoiceButton();
530 
531             // In order to properly configure the input method (if one is being used), we
532             // need to let it know if we'll be providing suggestions.  Although it would be
533             // difficult/expensive to know if every last detail has been configured properly, we
534             // can at least see if a suggestions provider has been configured, and use that
535             // as our trigger.
536             int inputType = mSearchable.getInputType();
537             // We only touch this if the input type is set up for text (which it almost certainly
538             // should be, in the case of search!)
539             if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
540                 // The existence of a suggestions authority is the proxy for "suggestions
541                 // are available here"
542                 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
543                 if (mSearchable.getSuggestAuthority() != null) {
544                     inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
545                 }
546             }
547             mSearchAutoComplete.setInputType(inputType);
548             mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
549             mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
550 
551             // If the search dialog is going to show a voice search button, then don't let
552             // the soft keyboard display a microphone button if it would have otherwise.
553             if (mSearchable.getVoiceSearchEnabled()) {
554                 mSearchAutoComplete.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
555             } else {
556                 mSearchAutoComplete.setPrivateImeOptions(null);
557             }
558         }
559     }
560 
561     /**
562      * Updates the auto-complete text view.
563      */
updateSearchAutoComplete()564     private void updateSearchAutoComplete() {
565         // close any existing suggestions adapter
566         closeSuggestionsAdapter();
567 
568         mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
569         mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
570         // we dismiss the entire dialog instead
571         mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
572 
573         if (!isInRealAppSearch()) {
574             mSearchAutoComplete.setDropDownAlwaysVisible(true);  // fill space until results come in
575         } else {
576             mSearchAutoComplete.setDropDownAlwaysVisible(false);
577         }
578 
579         mSearchAutoComplete.setForceIgnoreOutsideTouch(true);
580 
581         // attach the suggestions adapter, if suggestions are available
582         // The existence of a suggestions authority is the proxy for "suggestions available here"
583         if (mSearchable.getSuggestAuthority() != null) {
584             mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable,
585                     mOutsideDrawablesCache, mGlobalSearchMode);
586             mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
587         }
588     }
589 
590     /**
591      * Update the text in the search button.  Note: This is deprecated functionality, for
592      * 1.0 compatibility only.
593      */
updateSearchButton()594     private void updateSearchButton() {
595         String textLabel = null;
596         Drawable iconLabel = null;
597         int textId = mSearchable.getSearchButtonText();
598         if (textId != 0) {
599             textLabel = mActivityContext.getResources().getString(textId);
600         } else {
601             iconLabel = getContext().getResources().
602                     getDrawable(com.android.internal.R.drawable.ic_btn_search);
603         }
604         mGoButton.setText(textLabel);
605         mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
606     }
607 
updateSearchAppIcon()608     private void updateSearchAppIcon() {
609         // In Donut, we special-case the case of the browser to hide the app icon as if it were
610         // global search, for extra space for url entry.
611         //
612         // TODO: Remove this special case once the issue has been reconciled in Eclair.
613         if (mGlobalSearchMode || isBrowserSearch()) {
614             mAppIcon.setImageResource(0);
615             mAppIcon.setVisibility(View.GONE);
616             mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
617                     mSearchPlate.getPaddingTop(),
618                     mSearchPlate.getPaddingRight(),
619                     mSearchPlate.getPaddingBottom());
620         } else {
621             PackageManager pm = getContext().getPackageManager();
622             Drawable icon;
623             try {
624                 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
625                 icon = pm.getApplicationIcon(info.applicationInfo);
626                 if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
627             } catch (NameNotFoundException e) {
628                 icon = pm.getDefaultActivityIcon();
629                 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
630             }
631             mAppIcon.setImageDrawable(icon);
632             mAppIcon.setVisibility(View.VISIBLE);
633             mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
634                     mSearchPlate.getPaddingTop(),
635                     mSearchPlate.getPaddingRight(),
636                     mSearchPlate.getPaddingBottom());
637         }
638     }
639 
640     /**
641      * Setup the search "Badge" if requested by mode flags.
642      */
updateSearchBadge()643     private void updateSearchBadge() {
644         // assume both hidden
645         int visibility = View.GONE;
646         Drawable icon = null;
647         CharSequence text = null;
648 
649         // optionally show one or the other.
650         if (mSearchable.useBadgeIcon()) {
651             icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
652             visibility = View.VISIBLE;
653             if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
654         } else if (mSearchable.useBadgeLabel()) {
655             text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
656             visibility = View.VISIBLE;
657             if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
658         }
659 
660         mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
661         mBadgeLabel.setText(text);
662         mBadgeLabel.setVisibility(visibility);
663     }
664 
665     /**
666      * Update the hint in the query text field.
667      */
updateQueryHint()668     private void updateQueryHint() {
669         if (isShowing()) {
670             String hint = null;
671             if (mSearchable != null) {
672                 int hintId = mSearchable.getHintId();
673                 if (hintId != 0) {
674                     hint = mActivityContext.getString(hintId);
675                 }
676             }
677             mSearchAutoComplete.setHint(hint);
678         }
679     }
680 
681     /**
682      * Update the visibility of the voice button.  There are actually two voice search modes,
683      * either of which will activate the button.
684      */
updateVoiceButton()685     private void updateVoiceButton() {
686         int visibility = View.GONE;
687         if (mSearchable.getVoiceSearchEnabled()) {
688             Intent testIntent = null;
689             if (mSearchable.getVoiceSearchLaunchWebSearch()) {
690                 testIntent = mVoiceWebSearchIntent;
691             } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
692                 testIntent = mVoiceAppSearchIntent;
693             }
694             if (testIntent != null) {
695                 ResolveInfo ri = getContext().getPackageManager().
696                         resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
697                 if (ri != null) {
698                     visibility = View.VISIBLE;
699                 }
700             }
701         }
702         mVoiceButton.setVisibility(visibility);
703     }
704 
705     /**
706      * Hack to determine whether this is the browser, so we can remove the browser icon
707      * to the left of the search field, as a special requirement for Donut.
708      *
709      * TODO: For Eclair, reconcile this with the rest of the global search UI.
710      */
isBrowserSearch()711     private boolean isBrowserSearch() {
712         return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/");
713     }
714 
715     /**
716      * Listeners of various types
717      */
718 
719     /**
720      * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
721      * touch is outside the window. But the window includes space for the drop-down,
722      * so we also cancel on taps outside the search bar when the drop-down is not showing.
723      */
724     @Override
onTouchEvent(MotionEvent event)725     public boolean onTouchEvent(MotionEvent event) {
726         // cancel if the drop-down is not showing and the touch event was outside the search plate
727         if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
728             if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
729             cancel();
730             return true;
731         }
732         // Let Dialog handle events outside the window while the pop-up is showing.
733         return super.onTouchEvent(event);
734     }
735 
isOutOfBounds(View v, MotionEvent event)736     private boolean isOutOfBounds(View v, MotionEvent event) {
737         final int x = (int) event.getX();
738         final int y = (int) event.getY();
739         final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
740         return (x < -slop) || (y < -slop)
741                 || (x > (v.getWidth()+slop))
742                 || (y > (v.getHeight()+slop));
743     }
744 
745     /**
746      * Dialog's OnKeyListener implements various search-specific functionality
747      *
748      * @param keyCode This is the keycode of the typed key, and is the same value as
749      *        found in the KeyEvent parameter.
750      * @param event The complete event record for the typed key
751      *
752      * @return Return true if the event was handled here, or false if not.
753      */
754     @Override
onKeyDown(int keyCode, KeyEvent event)755     public boolean onKeyDown(int keyCode, KeyEvent event) {
756         if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
757         if (mSearchable == null) {
758             return false;
759         }
760 
761         if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() == 0) {
762             event.startTracking();
763             // Consume search key for later use.
764             return true;
765         }
766 
767         // if it's an action specified by the searchable activity, launch the
768         // entered query with the action key
769         SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
770         if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
771             launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
772             return true;
773         }
774 
775         return super.onKeyDown(keyCode, event);
776     }
777 
778     @Override
onKeyUp(int keyCode, KeyEvent event)779     public boolean onKeyUp(int keyCode, KeyEvent event) {
780         if (DBG) Log.d(LOG_TAG, "onKeyUp(" + keyCode + "," + event + ")");
781         if (mSearchable == null) {
782             return false;
783         }
784 
785         if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking()
786                 && !event.isCanceled()) {
787             // If the search key is pressed, toggle between global and in-app search. If we are
788             // currently doing global search and there is no in-app search context to toggle to,
789             // just don't do anything.
790             return toggleGlobalSearch();
791         }
792 
793         return super.onKeyUp(keyCode, event);
794     }
795 
796     /**
797      * Callback to watch the textedit field for empty/non-empty
798      */
799     private TextWatcher mTextWatcher = new TextWatcher() {
800 
801         public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
802 
803         public void onTextChanged(CharSequence s, int start,
804                 int before, int after) {
805             if (DBG_LOG_TIMING) {
806                 dbgLogTiming("onTextChanged()");
807             }
808             if (mSearchable == null) {
809                 return;
810             }
811             updateWidgetState();
812             if (!mSearchAutoComplete.isPerformingCompletion()) {
813                 // The user changed the query, remember it.
814                 mUserQuery = s == null ? "" : s.toString();
815             }
816         }
817 
818         public void afterTextChanged(Editable s) {
819             if (mSearchable == null) {
820                 return;
821             }
822             if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) {
823                 // The user changed the query, check if it is a URL and if so change the search
824                 // button in the soft keyboard to the 'Go' button.
825                 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION));
826                 if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) {
827                     options = options | EditorInfo.IME_ACTION_GO;
828                 } else {
829                     options = options | EditorInfo.IME_ACTION_SEARCH;
830                 }
831                 if (options != mSearchAutoCompleteImeOptions) {
832                     mSearchAutoCompleteImeOptions = options;
833                     mSearchAutoComplete.setImeOptions(options);
834                     // This call is required to update the soft keyboard UI with latest IME flags.
835                     mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
836                 }
837             }
838         }
839     };
840 
841     /**
842      * Enable/Disable the cancel button based on edit text state (any text?)
843      */
updateWidgetState()844     private void updateWidgetState() {
845         // enable the button if we have one or more non-space characters
846         boolean enabled = !mSearchAutoComplete.isEmpty();
847         mGoButton.setEnabled(enabled);
848         mGoButton.setFocusable(enabled);
849     }
850 
851     /**
852      * React to typing in the GO search button by refocusing to EditText.
853      * Continue typing the query.
854      */
855     View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
856         public boolean onKey(View v, int keyCode, KeyEvent event) {
857             // guard against possible race conditions
858             if (mSearchable == null) {
859                 return false;
860             }
861 
862             if (!event.isSystem() &&
863                     (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
864                     (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
865                     (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
866                     (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
867                 // restore focus and give key to EditText ...
868                 if (mSearchAutoComplete.requestFocus()) {
869                     return mSearchAutoComplete.dispatchKeyEvent(event);
870                 }
871             }
872 
873             return false;
874         }
875     };
876 
877     /**
878      * React to a click in the GO button by launching a search.
879      */
880     View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
881         public void onClick(View v) {
882             // guard against possible race conditions
883             if (mSearchable == null) {
884                 return;
885             }
886             launchQuerySearch();
887         }
888     };
889 
890     /**
891      * React to a click in the voice search button.
892      */
893     View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
894         public void onClick(View v) {
895             // guard against possible race conditions
896             if (mSearchable == null) {
897                 return;
898             }
899             try {
900                 // First stop the existing search before starting voice search, or else we'll end
901                 // up showing the search dialog again once we return to the app.
902                 ((SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE)).
903                         stopSearch();
904 
905                 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
906                     getContext().startActivity(mVoiceWebSearchIntent);
907                 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
908                     Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
909                     getContext().startActivity(appSearchIntent);
910                 }
911             } catch (ActivityNotFoundException e) {
912                 // Should not happen, since we check the availability of
913                 // voice search before showing the button. But just in case...
914                 Log.w(LOG_TAG, "Could not find voice search activity");
915             }
916          }
917     };
918 
919     /**
920      * Create and return an Intent that can launch the voice search activity, perform a specific
921      * voice transcription, and forward the results to the searchable activity.
922      *
923      * @param baseIntent The voice app search intent to start from
924      * @return A completely-configured intent ready to send to the voice search activity
925      */
createVoiceAppSearchIntent(Intent baseIntent)926     private Intent createVoiceAppSearchIntent(Intent baseIntent) {
927         ComponentName searchActivity = mSearchable.getSearchActivity();
928 
929         // create the necessary intent to set up a search-and-forward operation
930         // in the voice search system.   We have to keep the bundle separate,
931         // because it becomes immutable once it enters the PendingIntent
932         Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
933         queryIntent.setComponent(searchActivity);
934         PendingIntent pending = PendingIntent.getActivity(
935                 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
936 
937         // Now set up the bundle that will be inserted into the pending intent
938         // when it's time to do the search.  We always build it here (even if empty)
939         // because the voice search activity will always need to insert "QUERY" into
940         // it anyway.
941         Bundle queryExtras = new Bundle();
942         if (mAppSearchData != null) {
943             queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
944         }
945 
946         // Now build the intent to launch the voice search.  Add all necessary
947         // extras to launch the voice recognizer, and then all the necessary extras
948         // to forward the results to the searchable activity
949         Intent voiceIntent = new Intent(baseIntent);
950 
951         // Add all of the configuration options supplied by the searchable's metadata
952         String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
953         String prompt = null;
954         String language = null;
955         int maxResults = 1;
956         Resources resources = mActivityContext.getResources();
957         if (mSearchable.getVoiceLanguageModeId() != 0) {
958             languageModel = resources.getString(mSearchable.getVoiceLanguageModeId());
959         }
960         if (mSearchable.getVoicePromptTextId() != 0) {
961             prompt = resources.getString(mSearchable.getVoicePromptTextId());
962         }
963         if (mSearchable.getVoiceLanguageId() != 0) {
964             language = resources.getString(mSearchable.getVoiceLanguageId());
965         }
966         if (mSearchable.getVoiceMaxResults() != 0) {
967             maxResults = mSearchable.getVoiceMaxResults();
968         }
969         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
970         voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
971         voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
972         voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
973         voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
974                 searchActivity == null ? null : searchActivity.toShortString());
975 
976         // Add the values that configure forwarding the results
977         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
978         voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
979 
980         return voiceIntent;
981     }
982 
983     /**
984      * Corrects http/https typo errors in the given url string, and if the protocol specifier was
985      * not present defaults to http.
986      *
987      * @param inUrl URL to check and fix
988      * @return fixed URL string.
989      */
fixUrl(String inUrl)990     private String fixUrl(String inUrl) {
991         if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
992             return inUrl;
993 
994         if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
995             if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
996                 inUrl = inUrl.replaceFirst("/", "//");
997             } else {
998                 inUrl = inUrl.replaceFirst(":", "://");
999             }
1000         }
1001 
1002         if (inUrl.indexOf("://") == -1) {
1003             inUrl = "http://" + inUrl;
1004         }
1005 
1006         return inUrl;
1007     }
1008 
1009     /**
1010      * React to the user typing "enter" or other hardwired keys while typing in the search box.
1011      * This handles these special keys while the edit box has focus.
1012      */
1013     View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
1014         public boolean onKey(View v, int keyCode, KeyEvent event) {
1015             // guard against possible race conditions
1016             if (mSearchable == null) {
1017                 return false;
1018             }
1019 
1020             if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
1021             if (DBG) {
1022                 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
1023                         + "), selection: " + mSearchAutoComplete.getListSelection());
1024             }
1025 
1026             // If a suggestion is selected, handle enter, search key, and action keys
1027             // as presses on the selected suggestion
1028             if (mSearchAutoComplete.isPopupShowing() &&
1029                     mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
1030                 return onSuggestionsKey(v, keyCode, event);
1031             }
1032 
1033             // If there is text in the query box, handle enter, and action keys
1034             // The search key is handled by the dialog's onKeyDown().
1035             if (!mSearchAutoComplete.isEmpty()) {
1036                 if (keyCode == KeyEvent.KEYCODE_ENTER
1037                         && event.getAction() == KeyEvent.ACTION_UP) {
1038                     v.cancelLongPress();
1039 
1040                     // If this is a url entered by the user & we displayed the 'Go' button which
1041                     // the user clicked, launch the url instead of using it as a search query.
1042                     if (mSearchable.autoUrlDetect() &&
1043                         (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
1044                                 == EditorInfo.IME_ACTION_GO) {
1045                         Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
1046                         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1047                         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1048                         launchIntent(intent);
1049                     } else {
1050                         // Launch as a regular search.
1051                         launchQuerySearch();
1052                     }
1053                     return true;
1054                 }
1055                 if (event.getAction() == KeyEvent.ACTION_DOWN) {
1056                     SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1057                     if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
1058                         launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
1059                         return true;
1060                     }
1061                 }
1062             }
1063             return false;
1064         }
1065     };
1066 
1067     @Override
hide()1068     public void hide() {
1069         if (!isShowing()) return;
1070 
1071         // We made sure the IME was displayed, so also make sure it is closed
1072         // when we go away.
1073         InputMethodManager imm = (InputMethodManager)getContext()
1074                 .getSystemService(Context.INPUT_METHOD_SERVICE);
1075         if (imm != null) {
1076             imm.hideSoftInputFromWindow(
1077                     getWindow().getDecorView().getWindowToken(), 0);
1078         }
1079 
1080         super.hide();
1081     }
1082 
1083     /**
1084      * React to the user typing while in the suggestions list. First, check for action
1085      * keys. If not handled, try refocusing regular characters into the EditText.
1086      */
onSuggestionsKey(View v, int keyCode, KeyEvent event)1087     private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1088         // guard against possible race conditions (late arrival after dismiss)
1089         if (mSearchable == null) {
1090             return false;
1091         }
1092         if (mSuggestionsAdapter == null) {
1093             return false;
1094         }
1095         if (event.getAction() == KeyEvent.ACTION_DOWN) {
1096             if (DBG_LOG_TIMING) {
1097                 dbgLogTiming("onSuggestionsKey()");
1098             }
1099 
1100             // First, check for enter or search (both of which we'll treat as a "click")
1101             if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
1102                 int position = mSearchAutoComplete.getListSelection();
1103                 return launchSuggestion(position);
1104             }
1105 
1106             // Next, check for left/right moves, which we use to "return" the user to the edit view
1107             if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
1108                 // give "focus" to text editor, with cursor at the beginning if
1109                 // left key, at end if right key
1110                 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
1111                 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
1112                         0 : mSearchAutoComplete.length();
1113                 mSearchAutoComplete.setSelection(selPoint);
1114                 mSearchAutoComplete.setListSelection(0);
1115                 mSearchAutoComplete.clearListSelection();
1116                 mSearchAutoComplete.ensureImeVisible();
1117 
1118                 return true;
1119             }
1120 
1121             // Next, check for an "up and out" move
1122             if (keyCode == KeyEvent.KEYCODE_DPAD_UP
1123                     && 0 == mSearchAutoComplete.getListSelection()) {
1124                 restoreUserQuery();
1125                 // let ACTV complete the move
1126                 return false;
1127             }
1128 
1129             // Next, check for an "action key"
1130             SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1131             if ((actionKey != null) &&
1132                     ((actionKey.getSuggestActionMsg() != null) ||
1133                      (actionKey.getSuggestActionMsgColumn() != null))) {
1134                 // launch suggestion using action key column
1135                 int position = mSearchAutoComplete.getListSelection();
1136                 if (position != ListView.INVALID_POSITION) {
1137                     Cursor c = mSuggestionsAdapter.getCursor();
1138                     if (c.moveToPosition(position)) {
1139                         final String actionMsg = getActionKeyMessage(c, actionKey);
1140                         if (actionMsg != null && (actionMsg.length() > 0)) {
1141                             return launchSuggestion(position, keyCode, actionMsg);
1142                         }
1143                     }
1144                 }
1145             }
1146         }
1147         return false;
1148     }
1149 
1150     /**
1151      * Launch a search for the text in the query text field.
1152      */
launchQuerySearch()1153     public void launchQuerySearch()  {
1154         launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
1155     }
1156 
1157     /**
1158      * Launch a search for the text in the query text field.
1159      *
1160      * @param actionKey The key code of the action key that was pressed,
1161      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1162      * @param actionMsg The message for the action key that was pressed,
1163      *        or <code>null</code> if none.
1164      */
launchQuerySearch(int actionKey, String actionMsg)1165     protected void launchQuerySearch(int actionKey, String actionMsg)  {
1166         String query = mSearchAutoComplete.getText().toString();
1167         String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH;
1168         Intent intent = createIntent(action, null, null, query, null,
1169                 actionKey, actionMsg, null);
1170         // Allow GlobalSearch to log and create shortcut for searches launched by
1171         // the search button, enter key or an action key.
1172         if (mGlobalSearchMode) {
1173             mSuggestionsAdapter.reportSearch(query);
1174         }
1175         launchIntent(intent);
1176     }
1177 
1178     /**
1179      * Launches an intent based on a suggestion.
1180      *
1181      * @param position The index of the suggestion to create the intent from.
1182      * @return true if a successful launch, false if could not (e.g. bad position).
1183      */
launchSuggestion(int position)1184     protected boolean launchSuggestion(int position) {
1185         return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1186     }
1187 
1188     /**
1189      * Launches an intent based on a suggestion.
1190      *
1191      * @param position The index of the suggestion to create the intent from.
1192      * @param actionKey The key code of the action key that was pressed,
1193      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1194      * @param actionMsg The message for the action key that was pressed,
1195      *        or <code>null</code> if none.
1196      * @return true if a successful launch, false if could not (e.g. bad position).
1197      */
launchSuggestion(int position, int actionKey, String actionMsg)1198     protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1199         Cursor c = mSuggestionsAdapter.getCursor();
1200         if ((c != null) && c.moveToPosition(position)) {
1201 
1202             Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1203 
1204             // report back about the click
1205             if (mGlobalSearchMode) {
1206                 // in global search mode, do it via cursor
1207                 mSuggestionsAdapter.callCursorOnClick(c, position, actionKey, actionMsg);
1208             } else if (intent != null
1209                     && mPreviousComponents != null
1210                     && !mPreviousComponents.isEmpty()) {
1211                 // in-app search (and we have pivoted in as told by mPreviousComponents,
1212                 // which is used for keeping track of what we pop back to when we are pivoting into
1213                 // in app search.)
1214                 reportInAppClickToGlobalSearch(c, intent);
1215             }
1216 
1217             // launch the intent
1218             launchIntent(intent);
1219 
1220             return true;
1221         }
1222         return false;
1223     }
1224 
1225     /**
1226      * Report a click from an in app search result back to global search for shortcutting porpoises.
1227      *
1228      * @param c The cursor that is pointing to the clicked position.
1229      * @param intent The intent that will be launched for the click.
1230      */
reportInAppClickToGlobalSearch(Cursor c, Intent intent)1231     private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) {
1232         // for in app search, still tell global search via content provider
1233         Uri uri = getClickReportingUri();
1234         final ContentValues cv = new ContentValues();
1235         cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery);
1236         final ComponentName source = mSearchable.getSearchActivity();
1237         cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString());
1238 
1239         // grab the intent columns from the intent we created since it has additional
1240         // logic for falling back on the searchable default
1241         cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction());
1242         cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString());
1243         cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME,
1244                 intent.getComponent().flattenToShortString());
1245 
1246         // ensure the icons will work for global search
1247         cv.put(SearchManager.SUGGEST_COLUMN_ICON_1,
1248                         wrapIconForPackage(
1249                                 mSearchable.getSuggestPackage(),
1250                                 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1)));
1251         cv.put(SearchManager.SUGGEST_COLUMN_ICON_2,
1252                         wrapIconForPackage(
1253                                 mSearchable.getSuggestPackage(),
1254                                 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2)));
1255 
1256         // the rest can be passed through directly
1257         cv.put(SearchManager.SUGGEST_COLUMN_FORMAT,
1258                 getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT));
1259         cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
1260                 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1));
1261         cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
1262                 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2));
1263         cv.put(SearchManager.SUGGEST_COLUMN_QUERY,
1264                 getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY));
1265         cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
1266                 getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID));
1267         // note: deliberately omitting background color since it is only for global search
1268         // "more results" entries
1269         mContext.getContentResolver().insert(uri, cv);
1270     }
1271 
1272     /**
1273      * @return A URI appropriate for reporting a click.
1274      */
getClickReportingUri()1275     private Uri getClickReportingUri() {
1276         Uri.Builder uriBuilder = new Uri.Builder()
1277                 .scheme(ContentResolver.SCHEME_CONTENT)
1278                 .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY);
1279 
1280         uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH);
1281 
1282         return uriBuilder
1283                 .query("")     // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1284                 .fragment("")  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1285                 .build();
1286     }
1287 
1288     /**
1289      * Wraps an icon for a particular package.  If the icon is a resource id, it is converted into
1290      * an android.resource:// URI.
1291      *
1292      * @param packageName The source of the icon
1293      * @param icon The icon retrieved from a suggestion column
1294      * @return An icon string appropriate for the package.
1295      */
wrapIconForPackage(String packageName, String icon)1296     private String wrapIconForPackage(String packageName, String icon) {
1297         if (icon == null || icon.length() == 0 || "0".equals(icon)) {
1298             // SearchManager specifies that null or zero can be returned to indicate
1299             // no icon. We also allow empty string.
1300             return null;
1301         } else if (!Character.isDigit(icon.charAt(0))){
1302             return icon;
1303         } else {
1304             return new Uri.Builder()
1305                     .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
1306                     .authority(packageName)
1307                     .encodedPath(icon)
1308                     .toString();
1309         }
1310     }
1311 
1312     /**
1313      * Launches an intent, including any special intent handling.  Doesn't dismiss the dialog
1314      * since that will be handled in {@link SearchDialogWrapper#performActivityResuming}
1315      */
launchIntent(Intent intent)1316     private void launchIntent(Intent intent) {
1317         if (intent == null) {
1318             return;
1319         }
1320         if (handleSpecialIntent(intent)){
1321             return;
1322         }
1323         Log.d(LOG_TAG, "launching " + intent);
1324         try {
1325             // in global search mode, we send the activity straight to the original suggestion
1326             // source. this is because GlobalSearch may not have permission to launch the
1327             // intent, and to avoid the extra step of going through GlobalSearch.
1328             if (mGlobalSearchMode) {
1329                 launchGlobalSearchIntent(intent);
1330                 if (mStoredComponentName != null) {
1331                     // If we're embedded in an application, dismiss the dialog.
1332                     // This ensures that if the intent is handled by the current
1333                     // activity, it's not obscured by the dialog.
1334                     dismiss();
1335                 }
1336             } else {
1337                 // If the intent was created from a suggestion, it will always have an explicit
1338                 // component here.
1339                 Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI());
1340                 getContext().startActivity(intent);
1341                 // If the search switches to a different activity,
1342                 // SearchDialogWrapper#performActivityResuming
1343                 // will handle hiding the dialog when the next activity starts, but for
1344                 // real in-app search, we still need to dismiss the dialog.
1345                 if (isInRealAppSearch()) {
1346                     dismiss();
1347                 }
1348             }
1349         } catch (RuntimeException ex) {
1350             Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
1351         }
1352     }
1353 
launchGlobalSearchIntent(Intent intent)1354     private void launchGlobalSearchIntent(Intent intent) {
1355         final String packageName;
1356         // GlobalSearch puts the original source of the suggestion in the
1357         // 'component name' column. If set, we send the intent to that activity.
1358         // We trust GlobalSearch to always set this to the suggestion source.
1359         String intentComponent = intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY);
1360         if (intentComponent != null) {
1361             ComponentName componentName = ComponentName.unflattenFromString(intentComponent);
1362             intent.setComponent(componentName);
1363             intent.removeExtra(SearchManager.COMPONENT_NAME_KEY);
1364             // Launch the intent as the suggestion source.
1365             // This prevents sources from using the search dialog to launch
1366             // intents that they don't have permission for themselves.
1367             packageName = componentName.getPackageName();
1368         } else {
1369             // If there is no component in the suggestion, it must be a built-in suggestion
1370             // from GlobalSearch (e.g. "Search the web for") or the intent
1371             // launched when pressing the search/go button in the search dialog.
1372             // Launch the intent with the permissions of GlobalSearch.
1373             packageName = mSearchable.getSearchActivity().getPackageName();
1374         }
1375 
1376         // Launch all global search suggestions as new tasks, since they don't relate
1377         // to the current task.
1378         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1379         setBrowserApplicationId(intent);
1380 
1381         startActivityInPackage(intent, packageName);
1382     }
1383 
1384     /**
1385      * If the intent is to open an HTTP or HTTPS URL, we set
1386      * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that
1387      * has been opened by us for the same URL will be reused.
1388      */
setBrowserApplicationId(Intent intent)1389     private void setBrowserApplicationId(Intent intent) {
1390         Uri data = intent.getData();
1391         if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) {
1392             String scheme = data.getScheme();
1393             if (scheme != null && scheme.startsWith("http")) {
1394                 intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString());
1395             }
1396         }
1397     }
1398 
1399     /**
1400      * Starts an activity as if it had been started by the given package.
1401      *
1402      * @param intent The description of the activity to start.
1403      * @param packageName
1404      * @throws ActivityNotFoundException If the intent could not be resolved to
1405      *         and existing activity.
1406      * @throws SecurityException If the package does not have permission to start
1407      *         start the activity.
1408      * @throws AndroidRuntimeException If some other error occurs.
1409      */
startActivityInPackage(Intent intent, String packageName)1410     private void startActivityInPackage(Intent intent, String packageName) {
1411         try {
1412             int uid = ActivityThread.getPackageManager().getPackageUid(packageName);
1413             if (uid < 0) {
1414                 throw new AndroidRuntimeException("Package UID not found " + packageName);
1415             }
1416             String resolvedType = intent.resolveTypeIfNeeded(getContext().getContentResolver());
1417             IBinder resultTo = null;
1418             String resultWho = null;
1419             int requestCode = -1;
1420             boolean onlyIfNeeded = false;
1421             Log.i(LOG_TAG, "Starting (uid " + uid + ", " + packageName + ") " + intent.toURI());
1422             int result = ActivityManagerNative.getDefault().startActivityInPackage(
1423                     uid, intent, resolvedType, resultTo, resultWho, requestCode, onlyIfNeeded);
1424             checkStartActivityResult(result, intent);
1425         } catch (RemoteException ex) {
1426             throw new AndroidRuntimeException(ex);
1427         }
1428     }
1429 
1430     // Stolen from Instrumentation.checkStartActivityResult()
checkStartActivityResult(int res, Intent intent)1431     private static void checkStartActivityResult(int res, Intent intent) {
1432         if (res >= IActivityManager.START_SUCCESS) {
1433             return;
1434         }
1435         switch (res) {
1436             case IActivityManager.START_INTENT_NOT_RESOLVED:
1437             case IActivityManager.START_CLASS_NOT_FOUND:
1438                 if (intent.getComponent() != null)
1439                     throw new ActivityNotFoundException(
1440                             "Unable to find explicit activity class "
1441                             + intent.getComponent().toShortString()
1442                             + "; have you declared this activity in your AndroidManifest.xml?");
1443                 throw new ActivityNotFoundException(
1444                         "No Activity found to handle " + intent);
1445             case IActivityManager.START_PERMISSION_DENIED:
1446                 throw new SecurityException("Not allowed to start activity "
1447                         + intent);
1448             case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
1449                 throw new AndroidRuntimeException(
1450                         "FORWARD_RESULT_FLAG used while also requesting a result");
1451             default:
1452                 throw new AndroidRuntimeException("Unknown error code "
1453                         + res + " when starting " + intent);
1454         }
1455     }
1456 
1457     /**
1458      * Handles the special intent actions declared in {@link SearchManager}.
1459      *
1460      * @return <code>true</code> if the intent was handled.
1461      */
handleSpecialIntent(Intent intent)1462     private boolean handleSpecialIntent(Intent intent) {
1463         String action = intent.getAction();
1464         if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
1465             handleChangeSourceIntent(intent);
1466             return true;
1467         }
1468         return false;
1469     }
1470 
1471     /**
1472      * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}.
1473      */
handleChangeSourceIntent(Intent intent)1474     private void handleChangeSourceIntent(Intent intent) {
1475         Uri dataUri = intent.getData();
1476         if (dataUri == null) {
1477             Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
1478             return;
1479         }
1480         ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
1481         if (componentName == null) {
1482             Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
1483             return;
1484         }
1485         if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);
1486 
1487         pushPreviousComponent(mLaunchComponent);
1488         if (!show(componentName, mAppSearchData, false)) {
1489             Log.w(LOG_TAG, "Failed to switch to source " + componentName);
1490             popPreviousComponent();
1491             return;
1492         }
1493 
1494         String query = intent.getStringExtra(SearchManager.QUERY);
1495         setUserQuery(query);
1496         mSearchAutoComplete.showDropDown();
1497     }
1498 
1499     /**
1500      * Sets the list item selection in the AutoCompleteTextView's ListView.
1501      */
setListSelection(int index)1502     public void setListSelection(int index) {
1503         mSearchAutoComplete.setListSelection(index);
1504     }
1505 
1506     /**
1507      * Checks if there are any previous searchable components in the history stack.
1508      */
hasPreviousComponent()1509     private boolean hasPreviousComponent() {
1510         return mPreviousComponents != null && !mPreviousComponents.isEmpty();
1511     }
1512 
1513     /**
1514      * Saves the previous component that was searched, so that we can go
1515      * back to it.
1516      */
pushPreviousComponent(ComponentName componentName)1517     private void pushPreviousComponent(ComponentName componentName) {
1518         if (mPreviousComponents == null) {
1519             mPreviousComponents = new ArrayList<ComponentName>();
1520         }
1521         mPreviousComponents.add(componentName);
1522     }
1523 
1524     /**
1525      * Pops the previous component off the stack and returns it.
1526      *
1527      * @return The component name, or <code>null</code> if there was
1528      *         no previous component.
1529      */
popPreviousComponent()1530     private ComponentName popPreviousComponent() {
1531         if (!hasPreviousComponent()) {
1532             return null;
1533         }
1534         return mPreviousComponents.remove(mPreviousComponents.size() - 1);
1535     }
1536 
1537     /**
1538      * Goes back to the previous component that was searched, if any.
1539      *
1540      * @return <code>true</code> if there was a previous component that we could go back to.
1541      */
backToPreviousComponent()1542     private boolean backToPreviousComponent() {
1543         ComponentName previous = popPreviousComponent();
1544         if (previous == null) {
1545             return false;
1546         }
1547 
1548         if (!show(previous, mAppSearchData, false)) {
1549             Log.w(LOG_TAG, "Failed to switch to source " + previous);
1550             return false;
1551         }
1552 
1553         // must touch text to trigger suggestions
1554         // TODO: should this be the text as it was when the user left
1555         // the source that we are now going back to?
1556         String query = mSearchAutoComplete.getText().toString();
1557         setUserQuery(query);
1558         return true;
1559     }
1560 
1561     /**
1562      * When a particular suggestion has been selected, perform the various lookups required
1563      * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
1564      * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
1565      * the suggestion includes a data id.
1566      *
1567      * @param c The suggestions cursor, moved to the row of the user's selection
1568      * @param actionKey The key code of the action key that was pressed,
1569      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1570      * @param actionMsg The message for the action key that was pressed,
1571      *        or <code>null</code> if none.
1572      * @return An intent for the suggestion at the cursor's position.
1573      */
createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg)1574     private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
1575         try {
1576             // use specific action if supplied, or default action if supplied, or fixed default
1577             String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
1578 
1579             // some items are display only, or have effect via the cursor respond click reporting.
1580             if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
1581                 return null;
1582             }
1583 
1584             if (action == null) {
1585                 action = mSearchable.getSuggestIntentAction();
1586             }
1587             if (action == null) {
1588                 action = Intent.ACTION_SEARCH;
1589             }
1590 
1591             // use specific data if supplied, or default data if supplied
1592             String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
1593             if (data == null) {
1594                 data = mSearchable.getSuggestIntentData();
1595             }
1596             // then, if an ID was provided, append it.
1597             if (data != null) {
1598                 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1599                 if (id != null) {
1600                     data = data + "/" + Uri.encode(id);
1601                 }
1602             }
1603             Uri dataUri = (data == null) ? null : Uri.parse(data);
1604 
1605             String componentName = getColumnString(
1606                     c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
1607 
1608             String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
1609             String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
1610             String mode = mGlobalSearchMode ? SearchManager.MODE_GLOBAL_SEARCH_SUGGESTION : null;
1611 
1612             return createIntent(action, dataUri, extraData, query, componentName, actionKey,
1613                     actionMsg, mode);
1614         } catch (RuntimeException e ) {
1615             int rowNum;
1616             try {                       // be really paranoid now
1617                 rowNum = c.getPosition();
1618             } catch (RuntimeException e2 ) {
1619                 rowNum = -1;
1620             }
1621             Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1622                             " returned exception" + e.toString());
1623             return null;
1624         }
1625     }
1626 
1627     /**
1628      * Constructs an intent from the given information and the search dialog state.
1629      *
1630      * @param action Intent action.
1631      * @param data Intent data, or <code>null</code>.
1632      * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
1633      * @param query Intent query, or <code>null</code>.
1634      * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
1635      * @param actionKey The key code of the action key that was pressed,
1636      *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1637      * @param actionMsg The message for the action key that was pressed,
1638      *        or <code>null</code> if none.
1639      * @param mode The search mode, one of the acceptable values for
1640      *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
1641      * @return The intent.
1642      */
createIntent(String action, Uri data, String extraData, String query, String componentName, int actionKey, String actionMsg, String mode)1643     private Intent createIntent(String action, Uri data, String extraData, String query,
1644             String componentName, int actionKey, String actionMsg, String mode) {
1645         // Now build the Intent
1646         Intent intent = new Intent(action);
1647         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1648         // We need CLEAR_TOP to avoid reusing an old task that has other activities
1649         // on top of the one we want. We don't want to do this in in-app search though,
1650         // as it can be destructive to the activity stack.
1651         if (mGlobalSearchMode) {
1652             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1653         }
1654         if (data != null) {
1655             intent.setData(data);
1656         }
1657         intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
1658         if (query != null) {
1659             intent.putExtra(SearchManager.QUERY, query);
1660         }
1661         if (extraData != null) {
1662             intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1663         }
1664         if (componentName != null) {
1665             intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName);
1666         }
1667         if (mAppSearchData != null) {
1668             intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1669         }
1670         if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1671             intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1672             intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1673         }
1674         if (mode != null) {
1675             intent.putExtra(SearchManager.SEARCH_MODE, mode);
1676         }
1677         // Only allow 3rd-party intents from GlobalSearch
1678         if (!mGlobalSearchMode) {
1679             intent.setComponent(mSearchable.getSearchActivity());
1680         }
1681         return intent;
1682     }
1683 
1684     /**
1685      * For a given suggestion and a given cursor row, get the action message.  If not provided
1686      * by the specific row/column, also check for a single definition (for the action key).
1687      *
1688      * @param c The cursor providing suggestions
1689      * @param actionKey The actionkey record being examined
1690      *
1691      * @return Returns a string, or null if no action key message for this suggestion
1692      */
getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey)1693     private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
1694         String result = null;
1695         // check first in the cursor data, for a suggestion-specific message
1696         final String column = actionKey.getSuggestActionMsgColumn();
1697         if (column != null) {
1698             result = SuggestionsAdapter.getColumnString(c, column);
1699         }
1700         // If the cursor didn't give us a message, see if there's a single message defined
1701         // for the actionkey (for all suggestions)
1702         if (result == null) {
1703             result = actionKey.getSuggestActionMsg();
1704         }
1705         return result;
1706     }
1707 
1708     /**
1709      * The root element in the search bar layout. This is a custom view just to override
1710      * the handling of the back button.
1711      */
1712     public static class SearchBar extends LinearLayout {
1713 
1714         private SearchDialog mSearchDialog;
1715 
SearchBar(Context context, AttributeSet attrs)1716         public SearchBar(Context context, AttributeSet attrs) {
1717             super(context, attrs);
1718         }
1719 
SearchBar(Context context)1720         public SearchBar(Context context) {
1721             super(context);
1722         }
1723 
setSearchDialog(SearchDialog searchDialog)1724         public void setSearchDialog(SearchDialog searchDialog) {
1725             mSearchDialog = searchDialog;
1726         }
1727 
1728         /**
1729          * Overrides the handling of the back key to move back to the previous sources or dismiss
1730          * the search dialog, instead of dismissing the input method.
1731          */
1732         @Override
dispatchKeyEventPreIme(KeyEvent event)1733         public boolean dispatchKeyEventPreIme(KeyEvent event) {
1734             if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")");
1735             if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
1736                 KeyEvent.DispatcherState state = getKeyDispatcherState();
1737                 if (state != null) {
1738                     if (event.getAction() == KeyEvent.ACTION_DOWN
1739                             && event.getRepeatCount() == 0) {
1740                         state.startTracking(event, this);
1741                         return true;
1742                     } else if (event.getAction() == KeyEvent.ACTION_UP
1743                             && !event.isCanceled() && state.isTracking(event)) {
1744                         mSearchDialog.onBackPressed();
1745                         return true;
1746                     }
1747                 }
1748             }
1749             return super.dispatchKeyEventPreIme(event);
1750         }
1751     }
1752 
1753     /**
1754      * Local subclass for AutoCompleteTextView.
1755      */
1756     public static class SearchAutoComplete extends AutoCompleteTextView {
1757 
1758         private int mThreshold;
1759 
SearchAutoComplete(Context context)1760         public SearchAutoComplete(Context context) {
1761             super(context);
1762             mThreshold = getThreshold();
1763         }
1764 
SearchAutoComplete(Context context, AttributeSet attrs)1765         public SearchAutoComplete(Context context, AttributeSet attrs) {
1766             super(context, attrs);
1767             mThreshold = getThreshold();
1768         }
1769 
SearchAutoComplete(Context context, AttributeSet attrs, int defStyle)1770         public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1771             super(context, attrs, defStyle);
1772             mThreshold = getThreshold();
1773         }
1774 
1775         @Override
setThreshold(int threshold)1776         public void setThreshold(int threshold) {
1777             super.setThreshold(threshold);
1778             mThreshold = threshold;
1779         }
1780 
1781         /**
1782          * Returns true if the text field is empty, or contains only whitespace.
1783          */
isEmpty()1784         private boolean isEmpty() {
1785             return TextUtils.getTrimmedLength(getText()) == 0;
1786         }
1787 
1788         /**
1789          * We override this method to avoid replacing the query box text
1790          * when a suggestion is clicked.
1791          */
1792         @Override
replaceText(CharSequence text)1793         protected void replaceText(CharSequence text) {
1794         }
1795 
1796         /**
1797          * We override this method to avoid an extra onItemClick being called on the
1798          * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)}
1799          * when an item is clicked with the trackball.
1800          */
1801         @Override
performCompletion()1802         public void performCompletion() {
1803         }
1804 
1805         /**
1806          * We override this method to be sure and show the soft keyboard if appropriate when
1807          * the TextView has focus.
1808          */
1809         @Override
onWindowFocusChanged(boolean hasWindowFocus)1810         public void onWindowFocusChanged(boolean hasWindowFocus) {
1811             super.onWindowFocusChanged(hasWindowFocus);
1812 
1813             if (hasWindowFocus) {
1814                 InputMethodManager inputManager = (InputMethodManager)
1815                         getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1816                 inputManager.showSoftInput(this, 0);
1817             }
1818         }
1819 
1820         /**
1821          * We override this method so that we can allow a threshold of zero, which ACTV does not.
1822          */
1823         @Override
enoughToFilter()1824         public boolean enoughToFilter() {
1825             return mThreshold <= 0 || super.enoughToFilter();
1826         }
1827 
1828     }
1829 
1830     @Override
onBackPressed()1831     public void onBackPressed() {
1832         // If the input method is covering the search dialog completely,
1833         // e.g. in landscape mode with no hard keyboard, dismiss just the input method
1834         InputMethodManager imm = (InputMethodManager)getContext()
1835                 .getSystemService(Context.INPUT_METHOD_SERVICE);
1836         if (imm != null && imm.isFullscreenMode() &&
1837                 imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) {
1838             return;
1839         }
1840         // Otherwise, go back to any previous source (e.g. back to QSB when
1841         // pivoted into a source.
1842         if (!backToPreviousComponent()) {
1843             // If no previous source, close search dialog
1844             cancel();
1845         }
1846     }
1847 
1848     /**
1849      * Implements OnItemClickListener
1850      */
onItemClick(AdapterView<?> parent, View view, int position, long id)1851     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1852         if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1853         launchSuggestion(position);
1854     }
1855 
1856     /**
1857      * Implements OnItemSelectedListener
1858      */
onItemSelected(AdapterView<?> parent, View view, int position, long id)1859      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
1860          if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1861          // A suggestion has been selected, rewrite the query if possible,
1862          // otherwise the restore the original query.
1863          if (REWRITE_QUERIES) {
1864              rewriteQueryFromSuggestion(position);
1865          }
1866      }
1867 
1868      /**
1869       * Implements OnItemSelectedListener
1870       */
onNothingSelected(AdapterView<?> parent)1871      public void onNothingSelected(AdapterView<?> parent) {
1872          if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
1873      }
1874 
1875      /**
1876       * Query rewriting.
1877       */
1878 
rewriteQueryFromSuggestion(int position)1879      private void rewriteQueryFromSuggestion(int position) {
1880          Cursor c = mSuggestionsAdapter.getCursor();
1881          if (c == null) {
1882              return;
1883          }
1884          if (c.moveToPosition(position)) {
1885              // Get the new query from the suggestion.
1886              CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1887              if (newQuery != null) {
1888                  // The suggestion rewrites the query.
1889                  if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
1890                  // Update the text field, without getting new suggestions.
1891                  setQuery(newQuery);
1892              } else {
1893                  // The suggestion does not rewrite the query, restore the user's query.
1894                  if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
1895                  restoreUserQuery();
1896              }
1897          } else {
1898              // We got a bad position, restore the user's query.
1899              Log.w(LOG_TAG, "Bad suggestion position: " + position);
1900              restoreUserQuery();
1901          }
1902      }
1903 
1904      /**
1905       * Restores the query entered by the user if needed.
1906       */
restoreUserQuery()1907      private void restoreUserQuery() {
1908          if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
1909          setQuery(mUserQuery);
1910      }
1911 
1912      /**
1913       * Sets the text in the query box, without updating the suggestions.
1914       */
setQuery(CharSequence query)1915      private void setQuery(CharSequence query) {
1916          mSearchAutoComplete.setText(query, false);
1917          if (query != null) {
1918              mSearchAutoComplete.setSelection(query.length());
1919          }
1920      }
1921 
1922      /**
1923       * Sets the text in the query box, updating the suggestions.
1924       */
setUserQuery(String query)1925      private void setUserQuery(String query) {
1926          if (query == null) {
1927              query = "";
1928          }
1929          mUserQuery = query;
1930          mSearchAutoComplete.setText(query);
1931          mSearchAutoComplete.setSelection(query.length());
1932      }
1933 
1934     /**
1935      * Debugging Support
1936      */
1937 
1938     /**
1939      * For debugging only, sample the millisecond clock and log it.
1940      * Uses AtomicLong so we can use in multiple threads
1941      */
1942     private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
dbgLogTiming(final String caller)1943     private void dbgLogTiming(final String caller) {
1944         long millis = SystemClock.uptimeMillis();
1945         long oldTime = mLastLogTime.getAndSet(millis);
1946         long delta = millis - oldTime;
1947         final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
1948         Log.d(LOG_TAG,report);
1949     }
1950 }
1951