blob: 44d1eaa30272051400c673a9e452425d92edc13a [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.app;
18
Karl Rosaen875d50a2009-04-23 19:00:21 -070019import static android.app.SuggestionsAdapter.getColumnString;
Bjorn Bringertc1f40962009-04-29 13:08:39 +010020
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.content.ActivityNotFoundException;
22import android.content.BroadcastReceiver;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
Karl Rosaend4c98c42009-06-09 17:05:54 +010027import android.content.ContentResolver;
28import android.content.ContentValues;
Karl Rosaen875d50a2009-04-23 19:00:21 -070029import android.content.pm.ActivityInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.content.pm.PackageManager;
Karl Rosaen98e333f2009-04-28 10:39:09 -070031import android.content.pm.ResolveInfo;
Bjorn Bringertc1f40962009-04-29 13:08:39 +010032import android.content.pm.PackageManager.NameNotFoundException;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033import android.content.res.Configuration;
34import android.content.res.Resources;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035import android.database.Cursor;
36import android.graphics.drawable.Drawable;
Romain Guyb5537c42009-06-30 12:39:18 -070037import android.graphics.drawable.Animatable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038import android.net.Uri;
39import android.os.Bundle;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040import android.os.SystemClock;
41import android.server.search.SearchableInfo;
42import android.speech.RecognizerIntent;
43import android.text.Editable;
44import android.text.InputType;
45import android.text.TextUtils;
46import android.text.TextWatcher;
Satish Sampath662df0b2009-06-22 23:16:07 +010047import android.text.util.Regex;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048import android.util.AttributeSet;
49import android.util.Log;
Mike LeBeaua97f4a12009-05-23 01:19:36 -050050import android.view.ContextThemeWrapper;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import android.view.Gravity;
52import android.view.KeyEvent;
Karl Rosaen875d50a2009-04-23 19:00:21 -070053import android.view.MotionEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054import android.view.View;
Karl Rosaen875d50a2009-04-23 19:00:21 -070055import android.view.ViewConfiguration;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056import android.view.ViewGroup;
57import android.view.Window;
58import android.view.WindowManager;
Satish Sampath662df0b2009-06-22 23:16:07 +010059import android.view.inputmethod.EditorInfo;
Dianne Hackborna8f556e2009-03-24 20:47:50 -070060import android.view.inputmethod.InputMethodManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061import android.widget.AdapterView;
62import android.widget.AutoCompleteTextView;
63import android.widget.Button;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064import android.widget.ImageButton;
Mike LeBeau1fd73232009-04-27 19:12:05 -070065import android.widget.ImageView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066import android.widget.ListView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080067import android.widget.TextView;
Bjorn Bringertc1f40962009-04-29 13:08:39 +010068import android.widget.AdapterView.OnItemClickListener;
69import android.widget.AdapterView.OnItemSelectedListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080070
Karl Rosaen875d50a2009-04-23 19:00:21 -070071import java.util.ArrayList;
72import java.util.WeakHashMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080073import java.util.concurrent.atomic.AtomicLong;
74
75/**
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +010076 * System search dialog. This is controlled by the
77 * SearchManagerService and runs in the system process.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078 *
79 * @hide
80 */
81public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
82
83 // Debugging support
Karl Rosaen875d50a2009-04-23 19:00:21 -070084 private static final boolean DBG = false;
85 private static final String LOG_TAG = "SearchDialog";
86 private static final boolean DBG_LOG_TIMING = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080087
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080088 private static final String INSTANCE_KEY_COMPONENT = "comp";
89 private static final String INSTANCE_KEY_APPDATA = "data";
90 private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +010091 private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp";
92 private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
93 private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev";
94 private static final String INSTANCE_KEY_USER_QUERY = "uQry";
95
Mike LeBeau1fd73232009-04-27 19:12:05 -070096 private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
97 private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
98
Karl Rosaen875d50a2009-04-23 19:00:21 -070099 // interaction with runtime
100 private IntentFilter mCloseDialogsFilter;
101 private IntentFilter mPackageFilter;
102
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800103 // views & widgets
104 private TextView mBadgeLabel;
Mike LeBeau1fd73232009-04-27 19:12:05 -0700105 private ImageView mAppIcon;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700106 private SearchAutoComplete mSearchAutoComplete;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800107 private Button mGoButton;
108 private ImageButton mVoiceButton;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700109 private View mSearchPlate;
Romain Guyf4f70462009-06-26 16:55:54 -0700110 private Drawable mWorkingSpinner;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111
112 // interaction with searchable application
Karl Rosaen875d50a2009-04-23 19:00:21 -0700113 private SearchableInfo mSearchable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800114 private ComponentName mLaunchComponent;
115 private Bundle mAppSearchData;
116 private boolean mGlobalSearchMode;
117 private Context mActivityContext;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700118
Mike LeBeaub3aab692009-04-30 02:09:09 -0700119 // Values we store to allow user to toggle between in-app search and global search.
120 private ComponentName mStoredComponentName;
121 private Bundle mStoredAppSearchData;
122
Karl Rosaen875d50a2009-04-23 19:00:21 -0700123 // stack of previous searchables, to support the BACK key after
124 // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.
125 // The top of the stack (= previous searchable) is the last element of the list,
126 // since adding and removing is efficient at the end of an ArrayList.
127 private ArrayList<ComponentName> mPreviousComponents;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800128
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800129 // For voice searching
130 private Intent mVoiceWebSearchIntent;
131 private Intent mVoiceAppSearchIntent;
132
133 // support for AutoCompleteTextView suggestions display
134 private SuggestionsAdapter mSuggestionsAdapter;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700135
136 // Whether to rewrite queries when selecting suggestions
Mike LeBeaucce7dbc2009-06-18 09:25:18 -0700137 private static final boolean REWRITE_QUERIES = true;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700138
139 // The query entered by the user. This is not changed when selecting a suggestion
140 // that modifies the contents of the text field. But if the user then edits
141 // the suggestion, the resulting string is saved.
142 private String mUserQuery;
143
144 // A weak map of drawables we've gotten from other packages, so we don't load them
145 // more than once.
146 private final WeakHashMap<String, Drawable> mOutsideDrawablesCache =
147 new WeakHashMap<String, Drawable>();
Satish Sampath662df0b2009-06-22 23:16:07 +0100148
149 // Last known IME options value for the search edit text.
150 private int mSearchAutoCompleteImeOptions;
151
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800152 /**
153 * Constructor - fires it up and makes it look like the search UI.
154 *
155 * @param context Application Context we can use for system acess
156 */
157 public SearchDialog(Context context) {
Mike LeBeaua97f4a12009-05-23 01:19:36 -0500158 super(context, com.android.internal.R.style.Theme_GlobalSearchBar);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800159 }
160
161 /**
162 * We create the search dialog just once, and it stays around (hidden)
163 * until activated by the user.
164 */
165 @Override
166 protected void onCreate(Bundle savedInstanceState) {
167 super.onCreate(savedInstanceState);
168
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800169 setContentView(com.android.internal.R.layout.search_bar);
170
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100171 Window theWindow = getWindow();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800172 WindowManager.LayoutParams lp = theWindow.getAttributes();
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100173 lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR;
174 lp.width = ViewGroup.LayoutParams.FILL_PARENT;
175 // taking up the whole window (even when transparent) is less than ideal,
176 // but necessary to show the popup window until the window manager supports
177 // having windows anchored by their parent but not clipped by them.
178 lp.height = ViewGroup.LayoutParams.FILL_PARENT;
179 lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
Mike LeBeau98acd542009-05-07 19:04:39 -0700180 lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800181 theWindow.setAttributes(lp);
182
183 // get the view elements for local access
184 mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700185 mSearchAutoComplete = (SearchAutoComplete)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800186 findViewById(com.android.internal.R.id.search_src_text);
Mike LeBeau1fd73232009-04-27 19:12:05 -0700187 mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800188 mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
189 mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700190 mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
Romain Guyf4f70462009-06-26 16:55:54 -0700191 mWorkingSpinner = getContext().getResources().
Mike LeBeau1480eb22009-05-20 17:22:13 -0700192 getDrawable(com.android.internal.R.drawable.search_spinner);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800193
194 // attach listeners
Karl Rosaen875d50a2009-04-23 19:00:21 -0700195 mSearchAutoComplete.addTextChangedListener(mTextWatcher);
196 mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
197 mSearchAutoComplete.setOnItemClickListener(this);
198 mSearchAutoComplete.setOnItemSelectedListener(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800199 mGoButton.setOnClickListener(mGoButtonClickListener);
200 mGoButton.setOnKeyListener(mButtonsKeyListener);
201 mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
202 mVoiceButton.setOnKeyListener(mButtonsKeyListener);
203
Karl Rosaen875d50a2009-04-23 19:00:21 -0700204 mSearchAutoComplete.setSearchDialog(this);
205
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800206 // pre-hide all the extraneous elements
207 mBadgeLabel.setVisibility(View.GONE);
208
209 // Additional adjustments to make Dialog work for Search
210
211 // Touching outside of the search dialog will dismiss it
212 setCanceledOnTouchOutside(true);
213
214 // Set up broadcast filters
215 mCloseDialogsFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
216 mPackageFilter = new IntentFilter();
217 mPackageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
218 mPackageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
219 mPackageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
220 mPackageFilter.addDataScheme("package");
221
222 // Save voice intent for later queries/launching
223 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100224 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800225 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
226 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
227
228 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100229 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Satish Sampath662df0b2009-06-22 23:16:07 +0100230
231 mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800232 }
233
234 /**
235 * Set up the search dialog
236 *
Karl Rosaen875d50a2009-04-23 19:00:21 -0700237 * @return true if search dialog launched, false if not
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800238 */
239 public boolean show(String initialQuery, boolean selectInitialQuery,
240 ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100241
Mike LeBeaub3aab692009-04-30 02:09:09 -0700242 // Reset any stored values from last time dialog was shown.
243 mStoredComponentName = null;
244 mStoredAppSearchData = null;
245
246 return doShow(initialQuery, selectInitialQuery, componentName, appSearchData, globalSearch);
247 }
248
Mike LeBeaub3aab692009-04-30 02:09:09 -0700249 /**
250 * Called in response to a press of the hard search button in
251 * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app
252 * search and global search when relevant.
253 *
254 * If pressed within an in-app search context, this switches the search dialog out to
255 * global search. If pressed within a global search context that was originally an in-app
256 * search context, this switches back to the in-app search context. If pressed within a
257 * global search context that has no original in-app search context (e.g., global search
258 * from Home), this does nothing.
259 *
260 * @return false if we wanted to toggle context but could not do so successfully, true
261 * in all other cases
262 */
263 private boolean toggleGlobalSearch() {
264 String currentSearchText = mSearchAutoComplete.getText().toString();
265 if (!mGlobalSearchMode) {
266 mStoredComponentName = mLaunchComponent;
267 mStoredAppSearchData = mAppSearchData;
268 return doShow(currentSearchText, false, null, mAppSearchData, true);
269 } else {
270 if (mStoredComponentName != null) {
271 // This means we should toggle *back* to an in-app search context from
272 // global search.
273 return doShow(currentSearchText, false, mStoredComponentName,
274 mStoredAppSearchData, false);
275 } else {
276 return true;
277 }
278 }
279 }
280
281 /**
282 * Does the rest of the work required to show the search dialog. Called by both
283 * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and
284 * {@link #toggleGlobalSearch()}.
285 *
286 * @return true if search dialog showed, false if not
287 */
288 private boolean doShow(String initialQuery, boolean selectInitialQuery,
289 ComponentName componentName, Bundle appSearchData,
290 boolean globalSearch) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700291 // set up the searchable and show the dialog
292 if (!show(componentName, appSearchData, globalSearch)) {
293 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800294 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700295
Karl Rosaen875d50a2009-04-23 19:00:21 -0700296 // finally, load the user's initial text (which may trigger suggestions)
297 setUserQuery(initialQuery);
298 if (selectInitialQuery) {
299 mSearchAutoComplete.selectAll();
300 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700301
Karl Rosaen875d50a2009-04-23 19:00:21 -0700302 return true;
303 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700304
Karl Rosaen875d50a2009-04-23 19:00:21 -0700305 /**
306 * Sets up the search dialog and shows it.
307 *
308 * @return <code>true</code> if search dialog launched
309 */
310 private boolean show(ComponentName componentName, Bundle appSearchData,
311 boolean globalSearch) {
312
313 if (DBG) {
314 Log.d(LOG_TAG, "show(" + componentName + ", "
315 + appSearchData + ", " + globalSearch + ")");
316 }
317
Bjorn Bringert8d153822009-06-22 10:31:44 +0100318 SearchManager searchManager = (SearchManager)
319 mContext.getSystemService(Context.SEARCH_SERVICE);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700320 // Try to get the searchable info for the provided component (or for global search,
321 // if globalSearch == true).
Bjorn Bringert8d153822009-06-22 10:31:44 +0100322 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700323
324 // If we got back nothing, and it wasn't a request for global search, then try again
325 // for global search, as we'll try to launch that in lieu of any component-specific search.
326 if (!globalSearch && mSearchable == null) {
327 globalSearch = true;
Bjorn Bringert8d153822009-06-22 10:31:44 +0100328 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700329
330 // If we still get back null (i.e., there's not even a searchable info available
331 // for global search), then really give up.
332 if (mSearchable == null) {
333 // Unfortunately, we can't log here. it would be logspam every time the user
334 // clicks the "search" key on a non-search app.
335 return false;
336 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800337 }
338
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800339 mLaunchComponent = componentName;
340 mAppSearchData = appSearchData;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700341 // Using globalSearch here is just an optimization, just calling
342 // isDefaultSearchable() should always give the same result.
Bjorn Bringert8d153822009-06-22 10:31:44 +0100343 mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700344 mActivityContext = mSearchable.getActivityContext(getContext());
345
346 // show the dialog. this will call onStart().
347 if (!isShowing()) {
Mike LeBeaub059d902009-05-08 18:38:45 -0700348 // First make sure the keyboard is showing (if needed), so that we get the right height
349 // for the dropdown to respect the IME.
350 if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
351 Configuration.HARDKEYBOARDHIDDEN_YES) {
352 InputMethodManager inputManager = (InputMethodManager)
353 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
354 inputManager.showSoftInputUnchecked(0, null);
355 }
Mike LeBeaua97f4a12009-05-23 01:19:36 -0500356
357 // The Dialog uses a ContextThemeWrapper for the context; use this to change the
358 // theme out from underneath us, between the global search theme and the in-app
359 // search theme. They are identical except that the global search theme does not
360 // dim the background of the window (because global search is full screen so it's
361 // not needed and this should save a little bit of time on global search invocation).
362 Object context = getContext();
363 if (context instanceof ContextThemeWrapper) {
364 ContextThemeWrapper wrapper = (ContextThemeWrapper) context;
365 if (globalSearch) {
366 wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar);
367 } else {
368 wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar);
369 }
370 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700371 show();
372 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800373
Karl Rosaen875d50a2009-04-23 19:00:21 -0700374 updateUI();
375
376 return true;
377 }
378
379 @Override
380 protected void onStart() {
381 super.onStart();
382
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800383 // receive broadcasts
384 getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
385 getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800386 }
387
388 /**
389 * The search dialog is being dismissed, so handle all of the local shutdown operations.
390 *
391 * This function is designed to be idempotent so that dismiss() can be safely called at any time
392 * (even if already closed) and more likely to really dump any memory. No leaks!
393 */
394 @Override
395 public void onStop() {
396 super.onStop();
397
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800398 // stop receiving broadcasts (throws exception if none registered)
399 try {
400 getContext().unregisterReceiver(mBroadcastReceiver);
401 } catch (RuntimeException e) {
402 // This is OK - it just means we didn't have any registered
403 }
404
Karl Rosaen875d50a2009-04-23 19:00:21 -0700405 closeSuggestionsAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800406
407 // dump extra memory we're hanging on to
408 mLaunchComponent = null;
409 mAppSearchData = null;
410 mSearchable = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800411 mActivityContext = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800412 mUserQuery = null;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700413 mPreviousComponents = null;
414 }
Mike LeBeau1c690752009-05-20 20:20:26 -0700415
Mike LeBeau1c690752009-05-20 20:20:26 -0700416 /**
Mike LeBeau1480eb22009-05-20 17:22:13 -0700417 * Sets the search dialog to the 'working' state, which shows a working spinner in the
418 * right hand size of the text field.
419 *
420 * @param working true to show spinner, false to hide spinner
421 */
422 public void setWorking(boolean working) {
423 if (working) {
424 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
425 null, null, mWorkingSpinner, null);
Romain Guyb5537c42009-06-30 12:39:18 -0700426 ((Animatable) mWorkingSpinner).start();
Mike LeBeau1480eb22009-05-20 17:22:13 -0700427 } else {
428 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
429 null, null, null, null);
Romain Guyb5537c42009-06-30 12:39:18 -0700430 ((Animatable) mWorkingSpinner).stop();
Mike LeBeau1480eb22009-05-20 17:22:13 -0700431 }
432 }
433
434 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700435 * Closes and gets rid of the suggestions adapter.
436 */
437 private void closeSuggestionsAdapter() {
438 // remove the adapter from the autocomplete first, to avoid any updates
439 // when we drop the cursor
440 mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
441 // close any leftover cursor
442 if (mSuggestionsAdapter != null) {
443 mSuggestionsAdapter.changeCursor(null);
444 }
445 mSuggestionsAdapter = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800446 }
447
448 /**
449 * Save the minimal set of data necessary to recreate the search
450 *
451 * @return A bundle with the state of the dialog.
452 */
453 @Override
454 public Bundle onSaveInstanceState() {
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);
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +0100461 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800466 return bundle;
467 }
468
469 /**
470 * Restore the state of the dialog from a previously saved bundle.
Karl Rosaen875d50a2009-04-23 19:00:21 -0700471 *
472 * TODO: go through this and make sure that it saves everything that is saved
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800473 *
474 * @param savedInstanceState The state of the dialog previously saved by
475 * {@link #onSaveInstanceState()}.
476 */
477 @Override
478 public void onRestoreInstanceState(Bundle savedInstanceState) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800479 ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
480 Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
481 boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +0100482 ComponentName storedComponentName =
483 savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT);
484 Bundle storedAppSearchData =
485 savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA);
486 ArrayList<ComponentName> previousComponents =
487 savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS);
488 String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
489
490 // Set stored state
491 mStoredComponentName = storedComponentName;
492 mStoredAppSearchData = storedAppSearchData;
493 mPreviousComponents = previousComponents;
494
495 // show the dialog.
496 if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800497 // for some reason, we couldn't re-instantiate
498 return;
499 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800500 }
501
502 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700503 * Called after resources have changed, e.g. after screen rotation or locale change.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800504 */
505 public void onConfigurationChanged(Configuration newConfig) {
506 if (isShowing()) {
507 // Redraw (resources may have changed)
508 updateSearchButton();
Mike LeBeau1fd73232009-04-27 19:12:05 -0700509 updateSearchAppIcon();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800510 updateSearchBadge();
511 updateQueryHint();
512 }
513 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700514
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800515 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700516 * Update the UI according to the info in the current value of {@link #mSearchable}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800517 */
Karl Rosaen875d50a2009-04-23 19:00:21 -0700518 private void updateUI() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800519 if (mSearchable != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700520 updateSearchAutoComplete();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800521 updateSearchButton();
Mike LeBeau1fd73232009-04-27 19:12:05 -0700522 updateSearchAppIcon();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800523 updateSearchBadge();
524 updateQueryHint();
525 updateVoiceButton();
526
527 // In order to properly configure the input method (if one is being used), we
528 // need to let it know if we'll be providing suggestions. Although it would be
529 // difficult/expensive to know if every last detail has been configured properly, we
530 // can at least see if a suggestions provider has been configured, and use that
531 // as our trigger.
532 int inputType = mSearchable.getInputType();
533 // We only touch this if the input type is set up for text (which it almost certainly
534 // should be, in the case of search!)
535 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
536 // The existence of a suggestions authority is the proxy for "suggestions
537 // are available here"
538 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
539 if (mSearchable.getSuggestAuthority() != null) {
540 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
541 }
542 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700543 mSearchAutoComplete.setInputType(inputType);
Satish Sampath662df0b2009-06-22 23:16:07 +0100544 mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
545 mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700546 }
547 }
548
549 /**
550 * Updates the auto-complete text view.
551 */
552 private void updateSearchAutoComplete() {
553 // close any existing suggestions adapter
554 closeSuggestionsAdapter();
555
556 mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
Bjorn Bringert203464a2009-04-27 17:08:11 +0100557 mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100558 // we dismiss the entire dialog instead
559 mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700560
561 if (mGlobalSearchMode) {
562 mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in
Karl Rosaen875d50a2009-04-23 19:00:21 -0700563 } else {
564 mSearchAutoComplete.setDropDownAlwaysVisible(false);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700565 }
566
567 // attach the suggestions adapter, if suggestions are available
568 // The existence of a suggestions authority is the proxy for "suggestions available here"
569 if (mSearchable.getSuggestAuthority() != null) {
Mike LeBeau1480eb22009-05-20 17:22:13 -0700570 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable,
571 mOutsideDrawablesCache, mGlobalSearchMode);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700572 mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800573 }
574 }
575
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800576 /**
577 * Update the text in the search button. Note: This is deprecated functionality, for
578 * 1.0 compatibility only.
579 */
580 private void updateSearchButton() {
581 String textLabel = null;
582 Drawable iconLabel = null;
583 int textId = mSearchable.getSearchButtonText();
584 if (textId != 0) {
585 textLabel = mActivityContext.getResources().getString(textId);
586 } else {
587 iconLabel = getContext().getResources().
588 getDrawable(com.android.internal.R.drawable.ic_btn_search);
589 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700590 mGoButton.setText(textLabel);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800591 mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
592 }
593
Mike LeBeau1fd73232009-04-27 19:12:05 -0700594 private void updateSearchAppIcon() {
595 if (mGlobalSearchMode) {
596 mAppIcon.setImageResource(0);
597 mAppIcon.setVisibility(View.GONE);
598 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
599 mSearchPlate.getPaddingTop(),
600 mSearchPlate.getPaddingRight(),
601 mSearchPlate.getPaddingBottom());
602 } else {
603 PackageManager pm = getContext().getPackageManager();
Romain Guyf4f70462009-06-26 16:55:54 -0700604 Drawable icon;
Mike LeBeau1fd73232009-04-27 19:12:05 -0700605 try {
606 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
607 icon = pm.getApplicationIcon(info.applicationInfo);
608 if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
609 } catch (NameNotFoundException e) {
610 icon = pm.getDefaultActivityIcon();
611 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
612 }
613 mAppIcon.setImageDrawable(icon);
614 mAppIcon.setVisibility(View.VISIBLE);
615 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
616 mSearchPlate.getPaddingTop(),
617 mSearchPlate.getPaddingRight(),
618 mSearchPlate.getPaddingBottom());
619 }
620 }
621
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800622 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700623 * Setup the search "Badge" if requested by mode flags.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800624 */
625 private void updateSearchBadge() {
626 // assume both hidden
627 int visibility = View.GONE;
628 Drawable icon = null;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700629 CharSequence text = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800630
631 // optionally show one or the other.
Bjorn Bringerta9204132009-05-05 14:06:35 +0100632 if (mSearchable.useBadgeIcon()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800633 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
634 visibility = View.VISIBLE;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700635 if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
Bjorn Bringerta9204132009-05-05 14:06:35 +0100636 } else if (mSearchable.useBadgeLabel()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800637 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
638 visibility = View.VISIBLE;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700639 if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800640 }
641
642 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
643 mBadgeLabel.setText(text);
644 mBadgeLabel.setVisibility(visibility);
645 }
646
647 /**
648 * Update the hint in the query text field.
649 */
650 private void updateQueryHint() {
651 if (isShowing()) {
652 String hint = null;
653 if (mSearchable != null) {
654 int hintId = mSearchable.getHintId();
655 if (hintId != 0) {
656 hint = mActivityContext.getString(hintId);
657 }
658 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700659 mSearchAutoComplete.setHint(hint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800660 }
661 }
662
663 /**
664 * Update the visibility of the voice button. There are actually two voice search modes,
665 * either of which will activate the button.
666 */
667 private void updateVoiceButton() {
668 int visibility = View.GONE;
669 if (mSearchable.getVoiceSearchEnabled()) {
670 Intent testIntent = null;
671 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
672 testIntent = mVoiceWebSearchIntent;
673 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
674 testIntent = mVoiceAppSearchIntent;
675 }
676 if (testIntent != null) {
677 ResolveInfo ri = getContext().getPackageManager().
678 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
679 if (ri != null) {
680 visibility = View.VISIBLE;
681 }
682 }
683 }
684 mVoiceButton.setVisibility(visibility);
685 }
686
687 /**
688 * Listeners of various types
689 */
690
691 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700692 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
693 * touch is outside the window. But the window includes space for the drop-down,
694 * so we also cancel on taps outside the search bar when the drop-down is not showing.
695 */
696 @Override
697 public boolean onTouchEvent(MotionEvent event) {
698 // cancel if the drop-down is not showing and the touch event was outside the search plate
699 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
700 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
701 cancel();
702 return true;
703 }
704 // Let Dialog handle events outside the window while the pop-up is showing.
705 return super.onTouchEvent(event);
706 }
707
708 private boolean isOutOfBounds(View v, MotionEvent event) {
709 final int x = (int) event.getX();
710 final int y = (int) event.getY();
711 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
712 return (x < -slop) || (y < -slop)
713 || (x > (v.getWidth()+slop))
714 || (y > (v.getHeight()+slop));
715 }
716
717 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800718 * Dialog's OnKeyListener implements various search-specific functionality
719 *
720 * @param keyCode This is the keycode of the typed key, and is the same value as
Karl Rosaen875d50a2009-04-23 19:00:21 -0700721 * found in the KeyEvent parameter.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800722 * @param event The complete event record for the typed key
723 *
724 * @return Return true if the event was handled here, or false if not.
725 */
726 @Override
727 public boolean onKeyDown(int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700728 if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
729
730 // handle back key to go back to previous searchable, etc.
731 if (handleBackKey(keyCode, event)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800732 return true;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700733 }
734
Karl Rosaen875d50a2009-04-23 19:00:21 -0700735 if (keyCode == KeyEvent.KEYCODE_SEARCH) {
Mike LeBeaub3aab692009-04-30 02:09:09 -0700736 // If the search key is pressed, toggle between global and in-app search. If we are
737 // currently doing global search and there is no in-app search context to toggle to,
738 // just don't do anything.
739 return toggleGlobalSearch();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800740 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700741
742 // if it's an action specified by the searchable activity, launch the
743 // entered query with the action key
744 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
Bjorn Bringerta9204132009-05-05 14:06:35 +0100745 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
746 launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
Karl Rosaen875d50a2009-04-23 19:00:21 -0700747 return true;
748 }
749
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800750 return false;
751 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700752
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800753 /**
754 * Callback to watch the textedit field for empty/non-empty
755 */
756 private TextWatcher mTextWatcher = new TextWatcher() {
757
Karl Rosaen875d50a2009-04-23 19:00:21 -0700758 public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800759
760 public void onTextChanged(CharSequence s, int start,
761 int before, int after) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700762 if (DBG_LOG_TIMING) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800763 dbgLogTiming("onTextChanged()");
764 }
765 updateWidgetState();
Karl Rosaen875d50a2009-04-23 19:00:21 -0700766 if (!mSearchAutoComplete.isPerformingCompletion()) {
767 // The user changed the query, remember it.
768 mUserQuery = s == null ? "" : s.toString();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800769 }
770 }
771
Satish Sampath662df0b2009-06-22 23:16:07 +0100772 public void afterTextChanged(Editable s) {
773 if (!mSearchAutoComplete.isPerformingCompletion()) {
774 // The user changed the query, check if it is a URL and if so change the search
775 // button in the soft keyboard to the 'Go' button.
776 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION));
777 if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) {
778 options = options | EditorInfo.IME_ACTION_GO;
779 } else {
780 options = options | EditorInfo.IME_ACTION_SEARCH;
781 }
782 if (options != mSearchAutoCompleteImeOptions) {
783 mSearchAutoCompleteImeOptions = options;
784 mSearchAutoComplete.setImeOptions(options);
785 // This call is required to update the soft keyboard UI with latest IME flags.
786 mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
787 }
788 }
789 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800790 };
791
792 /**
793 * Enable/Disable the cancel button based on edit text state (any text?)
794 */
795 private void updateWidgetState() {
796 // enable the button if we have one or more non-space characters
Karl Rosaen875d50a2009-04-23 19:00:21 -0700797 boolean enabled = !mSearchAutoComplete.isEmpty();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800798 mGoButton.setEnabled(enabled);
799 mGoButton.setFocusable(enabled);
800 }
801
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800802 /**
803 * React to typing in the GO search button by refocusing to EditText.
804 * Continue typing the query.
805 */
806 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
807 public boolean onKey(View v, int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700808 // guard against possible race conditions
809 if (mSearchable == null) {
810 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800811 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700812
813 if (!event.isSystem() &&
814 (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
815 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
816 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
817 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
818 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
819 // restore focus and give key to EditText ...
820 if (mSearchAutoComplete.requestFocus()) {
821 return mSearchAutoComplete.dispatchKeyEvent(event);
822 }
823 }
824
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800825 return false;
826 }
827 };
828
829 /**
830 * React to a click in the GO button by launching a search.
831 */
832 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
833 public void onClick(View v) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700834 // guard against possible race conditions
835 if (mSearchable == null) {
836 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800837 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700838 launchQuerySearch();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800839 }
840 };
841
842 /**
843 * React to a click in the voice search button.
844 */
845 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
846 public void onClick(View v) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700847 // guard against possible race conditions
848 if (mSearchable == null) {
849 return;
850 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800851 try {
852 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
853 getContext().startActivity(mVoiceWebSearchIntent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800854 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
855 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
856 getContext().startActivity(appSearchIntent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800857 }
858 } catch (ActivityNotFoundException e) {
859 // Should not happen, since we check the availability of
860 // voice search before showing the button. But just in case...
861 Log.w(LOG_TAG, "Could not find voice search activity");
862 }
863 }
864 };
865
866 /**
867 * Create and return an Intent that can launch the voice search activity, perform a specific
868 * voice transcription, and forward the results to the searchable activity.
869 *
870 * @param baseIntent The voice app search intent to start from
871 * @return A completely-configured intent ready to send to the voice search activity
872 */
873 private Intent createVoiceAppSearchIntent(Intent baseIntent) {
874 // create the necessary intent to set up a search-and-forward operation
875 // in the voice search system. We have to keep the bundle separate,
876 // because it becomes immutable once it enters the PendingIntent
877 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
Bjorn Bringerta9204132009-05-05 14:06:35 +0100878 queryIntent.setComponent(mSearchable.getSearchActivity());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800879 PendingIntent pending = PendingIntent.getActivity(
880 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
881
882 // Now set up the bundle that will be inserted into the pending intent
883 // when it's time to do the search. We always build it here (even if empty)
884 // because the voice search activity will always need to insert "QUERY" into
885 // it anyway.
886 Bundle queryExtras = new Bundle();
887 if (mAppSearchData != null) {
888 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
889 }
890
891 // Now build the intent to launch the voice search. Add all necessary
892 // extras to launch the voice recognizer, and then all the necessary extras
893 // to forward the results to the searchable activity
894 Intent voiceIntent = new Intent(baseIntent);
895
896 // Add all of the configuration options supplied by the searchable's metadata
897 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
898 String prompt = null;
899 String language = null;
900 int maxResults = 1;
901 Resources resources = mActivityContext.getResources();
902 if (mSearchable.getVoiceLanguageModeId() != 0) {
903 languageModel = resources.getString(mSearchable.getVoiceLanguageModeId());
904 }
905 if (mSearchable.getVoicePromptTextId() != 0) {
906 prompt = resources.getString(mSearchable.getVoicePromptTextId());
907 }
908 if (mSearchable.getVoiceLanguageId() != 0) {
909 language = resources.getString(mSearchable.getVoiceLanguageId());
910 }
911 if (mSearchable.getVoiceMaxResults() != 0) {
912 maxResults = mSearchable.getVoiceMaxResults();
913 }
914 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
915 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
916 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
917 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
918
919 // Add the values that configure forwarding the results
920 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
921 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
922
923 return voiceIntent;
924 }
925
926 /**
Satish Sampath662df0b2009-06-22 23:16:07 +0100927 * Corrects http/https typo errors in the given url string, and if the protocol specifier was
928 * not present defaults to http.
929 *
930 * @param inUrl URL to check and fix
931 * @return fixed URL string.
932 */
933 private String fixUrl(String inUrl) {
934 if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
935 return inUrl;
936
937 if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
938 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
939 inUrl = inUrl.replaceFirst("/", "//");
940 } else {
941 inUrl = inUrl.replaceFirst(":", "://");
942 }
943 }
944
945 if (inUrl.indexOf("://") == -1) {
946 inUrl = "http://" + inUrl;
947 }
948
949 return inUrl;
950 }
951
952 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800953 * React to the user typing "enter" or other hardwired keys while typing in the search box.
954 * This handles these special keys while the edit box has focus.
955 */
956 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
957 public boolean onKey(View v, int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700958 // guard against possible race conditions
959 if (mSearchable == null) {
960 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800961 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700962
963 if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
964 if (DBG) {
965 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
966 + "), selection: " + mSearchAutoComplete.getListSelection());
967 }
968
969 // If a suggestion is selected, handle enter, search key, and action keys
970 // as presses on the selected suggestion
971 if (mSearchAutoComplete.isPopupShowing() &&
972 mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
973 return onSuggestionsKey(v, keyCode, event);
974 }
975
976 // If there is text in the query box, handle enter, and action keys
977 // The search key is handled by the dialog's onKeyDown().
978 if (!mSearchAutoComplete.isEmpty()) {
979 if (keyCode == KeyEvent.KEYCODE_ENTER
980 && event.getAction() == KeyEvent.ACTION_UP) {
981 v.cancelLongPress();
Satish Sampath662df0b2009-06-22 23:16:07 +0100982
983 // If this is a url entered by the user and we displayed the 'Go' button which
984 // the user clicked, launch the url instead of using it as a search query.
985 if ((mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
986 == EditorInfo.IME_ACTION_GO) {
987 Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
988 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
989 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
990 launchIntent(intent);
991 } else {
992 // Launch as a regular search.
993 launchQuerySearch();
994 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700995 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800996 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700997 if (event.getAction() == KeyEvent.ACTION_DOWN) {
998 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
Bjorn Bringerta9204132009-05-05 14:06:35 +0100999 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
1000 launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001001 return true;
1002 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001003 }
1004 }
1005 return false;
1006 }
1007 };
Karl Rosaen875d50a2009-04-23 19:00:21 -07001008
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001009 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001010 * When the ACTION_CLOSE_SYSTEM_DIALOGS intent is received, we should close ourselves
1011 * immediately, in order to allow a higher-priority UI to take over
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001012 * (e.g. phone call received).
Karl Rosaen875d50a2009-04-23 19:00:21 -07001013 *
1014 * When a package is added, removed or changed, our current context
1015 * may no longer be valid. This would only happen if a package is installed/removed exactly
1016 * when the search bar is open. So for now we're just going to close the search
1017 * bar.
1018 * Anything fancier would require some checks to see if the user's context was still valid.
1019 * Which would be messier.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001020 */
1021 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
1022 @Override
1023 public void onReceive(Context context, Intent intent) {
1024 String action = intent.getAction();
1025 if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
1026 cancel();
1027 } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)
1028 || Intent.ACTION_PACKAGE_REMOVED.equals(action)
1029 || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001030 cancel();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001031 }
1032 }
1033 };
1034
Dianne Hackborna8f556e2009-03-24 20:47:50 -07001035 @Override
1036 public void cancel() {
1037 // We made sure the IME was displayed, so also make sure it is closed
1038 // when we go away.
1039 InputMethodManager imm = (InputMethodManager)getContext()
1040 .getSystemService(Context.INPUT_METHOD_SERVICE);
1041 if (imm != null) {
1042 imm.hideSoftInputFromWindow(
1043 getWindow().getDecorView().getWindowToken(), 0);
1044 }
1045
1046 super.cancel();
1047 }
1048
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001049 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001050 * React to the user typing while in the suggestions list. First, check for action
1051 * keys. If not handled, try refocusing regular characters into the EditText.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001052 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001053 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1054 // guard against possible race conditions (late arrival after dismiss)
1055 if (mSearchable == null) {
1056 return false;
1057 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001058 if (mSuggestionsAdapter == null) {
1059 return false;
1060 }
1061 if (event.getAction() == KeyEvent.ACTION_DOWN) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001062 if (DBG_LOG_TIMING) {
1063 dbgLogTiming("onSuggestionsKey()");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001064 }
1065
1066 // First, check for enter or search (both of which we'll treat as a "click")
1067 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001068 int position = mSearchAutoComplete.getListSelection();
1069 return launchSuggestion(position);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001070 }
1071
1072 // Next, check for left/right moves, which we use to "return" the user to the edit view
1073 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001074 // give "focus" to text editor, with cursor at the beginning if
1075 // left key, at end if right key
1076 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001077 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
Karl Rosaen875d50a2009-04-23 19:00:21 -07001078 0 : mSearchAutoComplete.length();
1079 mSearchAutoComplete.setSelection(selPoint);
1080 mSearchAutoComplete.setListSelection(0);
1081 mSearchAutoComplete.clearListSelection();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001082 return true;
1083 }
1084
1085 // Next, check for an "up and out" move
Karl Rosaen875d50a2009-04-23 19:00:21 -07001086 if (keyCode == KeyEvent.KEYCODE_DPAD_UP
1087 && 0 == mSearchAutoComplete.getListSelection()) {
1088 restoreUserQuery();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001089 // let ACTV complete the move
1090 return false;
1091 }
1092
1093 // Next, check for an "action key"
1094 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1095 if ((actionKey != null) &&
Bjorn Bringerta9204132009-05-05 14:06:35 +01001096 ((actionKey.getSuggestActionMsg() != null) ||
1097 (actionKey.getSuggestActionMsgColumn() != null))) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001098 // launch suggestion using action key column
1099 int position = mSearchAutoComplete.getListSelection();
1100 if (position != ListView.INVALID_POSITION) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001101 Cursor c = mSuggestionsAdapter.getCursor();
1102 if (c.moveToPosition(position)) {
1103 final String actionMsg = getActionKeyMessage(c, actionKey);
1104 if (actionMsg != null && (actionMsg.length() > 0)) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001105 return launchSuggestion(position, keyCode, actionMsg);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001106 }
1107 }
1108 }
1109 }
1110 }
1111 return false;
Karl Rosaen875d50a2009-04-23 19:00:21 -07001112 }
1113
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001114 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001115 * Launch a search for the text in the query text field.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001116 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001117 protected void launchQuerySearch() {
1118 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001119 }
1120
1121 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001122 * Launch a search for the text in the query text field.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001123 *
Karl Rosaen875d50a2009-04-23 19:00:21 -07001124 * @param actionKey The key code of the action key that was pressed,
1125 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1126 * @param actionMsg The message for the action key that was pressed,
1127 * or <code>null</code> if none.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001128 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001129 protected void launchQuerySearch(int actionKey, String actionMsg) {
1130 String query = mSearchAutoComplete.getText().toString();
Satish Sampathbf23fe02009-06-15 23:47:56 +01001131 Intent intent = createIntent(Intent.ACTION_SEARCH, null, null, query, null,
Karl Rosaen875d50a2009-04-23 19:00:21 -07001132 actionKey, actionMsg);
1133 launchIntent(intent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001134 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001135
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001136 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001137 * Launches an intent based on a suggestion.
1138 *
1139 * @param position The index of the suggestion to create the intent from.
1140 * @return true if a successful launch, false if could not (e.g. bad position).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001141 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001142 protected boolean launchSuggestion(int position) {
1143 return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1144 }
1145
1146 /**
1147 * Launches an intent based on a suggestion.
1148 *
1149 * @param position The index of the suggestion to create the intent from.
1150 * @param actionKey The key code of the action key that was pressed,
1151 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1152 * @param actionMsg The message for the action key that was pressed,
1153 * or <code>null</code> if none.
1154 * @return true if a successful launch, false if could not (e.g. bad position).
1155 */
1156 protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1157 Cursor c = mSuggestionsAdapter.getCursor();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001158 if ((c != null) && c.moveToPosition(position)) {
Karl Rosaend4c98c42009-06-09 17:05:54 +01001159
1160 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1161
1162 // report back about the click
1163 if (mGlobalSearchMode) {
1164 // in global search mode, do it via cursor
1165 mSuggestionsAdapter.callCursorOnClick(c, position);
1166 } else if (intent != null
1167 && mPreviousComponents != null
1168 && !mPreviousComponents.isEmpty()) {
1169 // in-app search (and we have pivoted in as told by mPreviousComponents,
1170 // which is used for keeping track of what we pop back to when we are pivoting into
1171 // in app search.)
1172 reportInAppClickToGlobalSearch(c, intent);
1173 }
Karl Rosaena058f022009-06-01 23:11:44 +01001174
1175 // launch the intent
Karl Rosaen875d50a2009-04-23 19:00:21 -07001176 launchIntent(intent);
Karl Rosaena058f022009-06-01 23:11:44 +01001177
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001178 return true;
1179 }
1180 return false;
1181 }
Karl Rosaena058f022009-06-01 23:11:44 +01001182
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001183 /**
Karl Rosaend4c98c42009-06-09 17:05:54 +01001184 * Report a click from an in app search result back to global search for shortcutting porpoises.
1185 *
1186 * @param c The cursor that is pointing to the clicked position.
1187 * @param intent The intent that will be launched for the click.
1188 */
1189 private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) {
1190 // for in app search, still tell global search via content provider
1191 Uri uri = getClickReportingUri();
1192 final ContentValues cv = new ContentValues();
1193 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery);
1194 final ComponentName source = mSearchable.getSearchActivity();
1195 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString());
1196
1197 // grab the intent columns from the intent we created since it has additional
1198 // logic for falling back on the searchable default
1199 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction());
1200 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString());
Satish Sampathbf23fe02009-06-15 23:47:56 +01001201 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME,
1202 intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY));
Karl Rosaend4c98c42009-06-09 17:05:54 +01001203
1204 // ensure the icons will work for global search
1205 cv.put(SearchManager.SUGGEST_COLUMN_ICON_1,
1206 wrapIconForPackage(
1207 source,
1208 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1)));
1209 cv.put(SearchManager.SUGGEST_COLUMN_ICON_2,
1210 wrapIconForPackage(
1211 source,
1212 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2)));
1213
1214 // the rest can be passed through directly
1215 cv.put(SearchManager.SUGGEST_COLUMN_FORMAT,
1216 getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT));
1217 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
1218 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1));
1219 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
1220 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2));
1221 cv.put(SearchManager.SUGGEST_COLUMN_QUERY,
1222 getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY));
1223 cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
1224 getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID));
1225 // note: deliberately omitting background color since it is only for global search
1226 // "more results" entries
1227 mContext.getContentResolver().insert(uri, cv);
1228 }
1229
1230 /**
1231 * @return A URI appropriate for reporting a click.
1232 */
1233 private Uri getClickReportingUri() {
1234 Uri.Builder uriBuilder = new Uri.Builder()
1235 .scheme(ContentResolver.SCHEME_CONTENT)
1236 .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY);
1237
1238 uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH);
1239
1240 return uriBuilder
1241 .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1242 .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1243 .build();
1244 }
1245
1246 /**
1247 * Wraps an icon for a particular package. If the icon is a resource id, it is converted into
1248 * an android.resource:// URI.
1249 *
1250 * @param source The source of the icon
1251 * @param icon The icon retrieved from a suggestion column
1252 * @return An icon string appropriate for the package.
1253 */
1254 private String wrapIconForPackage(ComponentName source, String icon) {
1255 if (icon == null || icon.length() == 0 || "0".equals(icon)) {
1256 // SearchManager specifies that null or zero can be returned to indicate
1257 // no icon. We also allow empty string.
1258 return null;
1259 } else if (!Character.isDigit(icon.charAt(0))){
1260 return icon;
1261 } else {
1262 String packageName = source.getPackageName();
1263 return new Uri.Builder()
1264 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
1265 .authority(packageName)
1266 .encodedPath(icon)
1267 .toString();
1268 }
1269 }
1270
1271 /**
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001272 * Launches an intent and dismisses the search dialog (unless the intent
1273 * is one of the special intents that modifies the state of the search dialog).
Karl Rosaen875d50a2009-04-23 19:00:21 -07001274 */
1275 private void launchIntent(Intent intent) {
1276 if (intent == null) {
1277 return;
1278 }
1279 if (handleSpecialIntent(intent)){
1280 return;
1281 }
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001282 dismiss();
Karl Rosaen875d50a2009-04-23 19:00:21 -07001283 getContext().startActivity(intent);
1284 }
1285
1286 /**
1287 * Handles the special intent actions declared in {@link SearchManager}.
1288 *
1289 * @return <code>true</code> if the intent was handled.
1290 */
1291 private boolean handleSpecialIntent(Intent intent) {
1292 String action = intent.getAction();
1293 if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
1294 handleChangeSourceIntent(intent);
1295 return true;
Karl Rosaen875d50a2009-04-23 19:00:21 -07001296 }
1297 return false;
1298 }
1299
1300 /**
Karl Rosaend4c98c42009-06-09 17:05:54 +01001301 * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001302 */
1303 private void handleChangeSourceIntent(Intent intent) {
1304 Uri dataUri = intent.getData();
1305 if (dataUri == null) {
1306 Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
1307 return;
1308 }
1309 ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
1310 if (componentName == null) {
1311 Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
1312 return;
1313 }
1314 if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);
1315
1316 ComponentName previous = mLaunchComponent;
1317 if (!show(componentName, mAppSearchData, false)) {
1318 Log.w(LOG_TAG, "Failed to switch to source " + componentName);
1319 return;
1320 }
1321 pushPreviousComponent(previous);
1322
1323 String query = intent.getStringExtra(SearchManager.QUERY);
1324 setUserQuery(query);
Mike LeBeau35df87c2009-06-24 13:06:39 -07001325 mSearchAutoComplete.showDropDown();
Karl Rosaen875d50a2009-04-23 19:00:21 -07001326 }
Karl Rosaena058f022009-06-01 23:11:44 +01001327
Karl Rosaen875d50a2009-04-23 19:00:21 -07001328 /**
Mike LeBeauae9760b2009-06-01 21:53:09 +01001329 * Sets the list item selection in the AutoCompleteTextView's ListView.
1330 */
1331 public void setListSelection(int index) {
1332 mSearchAutoComplete.setListSelection(index);
1333 }
Karl Rosaena058f022009-06-01 23:11:44 +01001334
Mike LeBeauae9760b2009-06-01 21:53:09 +01001335 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001336 * Saves the previous component that was searched, so that we can go
1337 * back to it.
1338 */
1339 private void pushPreviousComponent(ComponentName componentName) {
1340 if (mPreviousComponents == null) {
1341 mPreviousComponents = new ArrayList<ComponentName>();
1342 }
1343 mPreviousComponents.add(componentName);
1344 }
1345
1346 /**
1347 * Pops the previous component off the stack and returns it.
1348 *
1349 * @return The component name, or <code>null</code> if there was
1350 * no previous component.
1351 */
1352 private ComponentName popPreviousComponent() {
1353 if (mPreviousComponents == null) {
1354 return null;
1355 }
1356 int size = mPreviousComponents.size();
1357 if (size == 0) {
1358 return null;
1359 }
1360 return mPreviousComponents.remove(size - 1);
1361 }
1362
1363 /**
1364 * Goes back to the previous component that was searched, if any.
1365 *
1366 * @return <code>true</code> if there was a previous component that we could go back to.
1367 */
1368 private boolean backToPreviousComponent() {
1369 ComponentName previous = popPreviousComponent();
1370 if (previous == null) {
1371 return false;
1372 }
1373 if (!show(previous, mAppSearchData, false)) {
1374 Log.w(LOG_TAG, "Failed to switch to source " + previous);
1375 return false;
1376 }
1377
1378 // must touch text to trigger suggestions
1379 // TODO: should this be the text as it was when the user left
1380 // the source that we are now going back to?
1381 String query = mSearchAutoComplete.getText().toString();
1382 setUserQuery(query);
1383
1384 return true;
1385 }
1386
1387 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001388 * When a particular suggestion has been selected, perform the various lookups required
1389 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1390 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1391 * the suggestion includes a data id.
1392 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001393 * @param c The suggestions cursor, moved to the row of the user's selection
Karl Rosaen875d50a2009-04-23 19:00:21 -07001394 * @param actionKey The key code of the action key that was pressed,
1395 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1396 * @param actionMsg The message for the action key that was pressed,
1397 * or <code>null</code> if none.
1398 * @return An intent for the suggestion at the cursor's position.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001399 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001400 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001401 try {
1402 // use specific action if supplied, or default action if supplied, or fixed default
Karl Rosaen875d50a2009-04-23 19:00:21 -07001403 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
Karl Rosaena058f022009-06-01 23:11:44 +01001404
1405 // some items are display only, or have effect via the cursor respond click reporting.
1406 if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
1407 return null;
1408 }
1409
Karl Rosaen875d50a2009-04-23 19:00:21 -07001410 if (action == null) {
1411 action = mSearchable.getSuggestIntentAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001412 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001413 if (action == null) {
1414 action = Intent.ACTION_SEARCH;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001415 }
1416
1417 // use specific data if supplied, or default data if supplied
Karl Rosaen875d50a2009-04-23 19:00:21 -07001418 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001419 if (data == null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001420 data = mSearchable.getSuggestIntentData();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001421 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001422 // then, if an ID was provided, append it.
1423 if (data != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001424 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1425 if (id != null) {
1426 data = data + "/" + Uri.encode(id);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001427 }
1428 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001429 Uri dataUri = (data == null) ? null : Uri.parse(data);
1430
Satish Sampathbf23fe02009-06-15 23:47:56 +01001431 String componentName = getColumnString(
1432 c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
Karl Rosaena058f022009-06-01 23:11:44 +01001433
Karl Rosaen875d50a2009-04-23 19:00:21 -07001434 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
Satish Sampathbf23fe02009-06-15 23:47:56 +01001435 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001436
Satish Sampathbf23fe02009-06-15 23:47:56 +01001437 return createIntent(action, dataUri, extraData, query, componentName, actionKey,
1438 actionMsg);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001439 } catch (RuntimeException e ) {
1440 int rowNum;
1441 try { // be really paranoid now
1442 rowNum = c.getPosition();
1443 } catch (RuntimeException e2 ) {
1444 rowNum = -1;
1445 }
1446 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1447 " returned exception" + e.toString());
Karl Rosaen875d50a2009-04-23 19:00:21 -07001448 return null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001449 }
1450 }
1451
1452 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001453 * Constructs an intent from the given information and the search dialog state.
1454 *
1455 * @param action Intent action.
1456 * @param data Intent data, or <code>null</code>.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001457 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
Satish Sampathbf23fe02009-06-15 23:47:56 +01001458 * @param query Intent query, or <code>null</code>.
1459 * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001460 * @param actionKey The key code of the action key that was pressed,
1461 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1462 * @param actionMsg The message for the action key that was pressed,
1463 * or <code>null</code> if none.
1464 * @return The intent.
1465 */
Satish Sampathbf23fe02009-06-15 23:47:56 +01001466 private Intent createIntent(String action, Uri data, String extraData, String query,
1467 String componentName, int actionKey, String actionMsg) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001468 // Now build the Intent
1469 Intent intent = new Intent(action);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001470 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001471 if (data != null) {
1472 intent.setData(data);
1473 }
Bjorn Bringert5f806052009-06-24 12:02:26 +01001474 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001475 if (query != null) {
1476 intent.putExtra(SearchManager.QUERY, query);
1477 }
1478 if (extraData != null) {
1479 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1480 }
Satish Sampathbf23fe02009-06-15 23:47:56 +01001481 if (componentName != null) {
1482 intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName);
1483 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001484 if (mAppSearchData != null) {
1485 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1486 }
1487 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1488 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1489 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1490 }
1491 // attempt to enforce security requirement (no 3rd-party intents)
Bjorn Bringerta9204132009-05-05 14:06:35 +01001492 intent.setComponent(mSearchable.getSearchActivity());
Karl Rosaen875d50a2009-04-23 19:00:21 -07001493 return intent;
1494 }
1495
1496 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001497 * For a given suggestion and a given cursor row, get the action message. If not provided
1498 * by the specific row/column, also check for a single definition (for the action key).
1499 *
1500 * @param c The cursor providing suggestions
1501 * @param actionKey The actionkey record being examined
1502 *
1503 * @return Returns a string, or null if no action key message for this suggestion
1504 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001505 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001506 String result = null;
1507 // check first in the cursor data, for a suggestion-specific message
Bjorn Bringerta9204132009-05-05 14:06:35 +01001508 final String column = actionKey.getSuggestActionMsgColumn();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001509 if (column != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001510 result = SuggestionsAdapter.getColumnString(c, column);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001511 }
1512 // If the cursor didn't give us a message, see if there's a single message defined
1513 // for the actionkey (for all suggestions)
1514 if (result == null) {
Bjorn Bringerta9204132009-05-05 14:06:35 +01001515 result = actionKey.getSuggestActionMsg();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001516 }
1517 return result;
1518 }
1519
1520 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001521 * Local subclass for AutoCompleteTextView.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001522 */
1523 public static class SearchAutoComplete extends AutoCompleteTextView {
1524
Karl Rosaen875d50a2009-04-23 19:00:21 -07001525 private int mThreshold;
1526 private SearchDialog mSearchDialog;
1527
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001528 public SearchAutoComplete(Context context) {
Marco Nelissen1746d6f2009-05-14 13:29:24 -07001529 super(context);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001530 mThreshold = getThreshold();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001531 }
1532
1533 public SearchAutoComplete(Context context, AttributeSet attrs) {
1534 super(context, attrs);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001535 mThreshold = getThreshold();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001536 }
1537
1538 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1539 super(context, attrs, defStyle);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001540 mThreshold = getThreshold();
1541 }
1542
1543 private void setSearchDialog(SearchDialog searchDialog) {
1544 mSearchDialog = searchDialog;
1545 }
1546
1547 @Override
1548 public void setThreshold(int threshold) {
1549 super.setThreshold(threshold);
1550 mThreshold = threshold;
1551 }
1552
1553 /**
1554 * Returns true if the text field is empty, or contains only whitespace.
1555 */
1556 private boolean isEmpty() {
1557 return TextUtils.getTrimmedLength(getText()) == 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001558 }
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001559
Karl Rosaen875d50a2009-04-23 19:00:21 -07001560 /**
1561 * We override this method to avoid replacing the query box text
1562 * when a suggestion is clicked.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001563 */
1564 @Override
Karl Rosaen875d50a2009-04-23 19:00:21 -07001565 protected void replaceText(CharSequence text) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001566 }
1567
1568 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001569 * We override this method so that we can allow a threshold of zero, which ACTV does not.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001570 */
1571 @Override
1572 public boolean enoughToFilter() {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001573 return mThreshold <= 0 || super.enoughToFilter();
1574 }
Karl Rosaen98e333f2009-04-28 10:39:09 -07001575
Karl Rosaen875d50a2009-04-23 19:00:21 -07001576 /**
1577 * {@link AutoCompleteTextView#onKeyPreIme(int, KeyEvent)}) dismisses the drop-down on BACK,
1578 * so we must override this method to modify the BACK behavior.
1579 */
1580 @Override
1581 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
Karl Rosaen98e333f2009-04-28 10:39:09 -07001582 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
1583 if (mSearchDialog.backToPreviousComponent()) {
1584 return true;
1585 }
1586 return false; // will dismiss soft keyboard if necessary
1587 }
1588 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001589 }
1590 }
1591
Karl Rosaen875d50a2009-04-23 19:00:21 -07001592 protected boolean handleBackKey(int keyCode, KeyEvent event) {
1593 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001594 if (backToPreviousComponent()) {
1595 return true;
1596 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001597 cancel();
1598 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001599 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001600 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001601 }
1602
1603 /**
1604 * Implements OnItemClickListener
1605 */
1606 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001607 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1608 launchSuggestion(position);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001609 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001610
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001611 /**
1612 * Implements OnItemSelectedListener
1613 */
1614 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001615 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1616 // A suggestion has been selected, rewrite the query if possible,
1617 // otherwise the restore the original query.
1618 if (REWRITE_QUERIES) {
1619 rewriteQueryFromSuggestion(position);
1620 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001621 }
1622
1623 /**
1624 * Implements OnItemSelectedListener
1625 */
1626 public void onNothingSelected(AdapterView<?> parent) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001627 if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
1628 }
1629
1630 /**
1631 * Query rewriting.
1632 */
1633
1634 private void rewriteQueryFromSuggestion(int position) {
1635 Cursor c = mSuggestionsAdapter.getCursor();
1636 if (c == null) {
1637 return;
1638 }
1639 if (c.moveToPosition(position)) {
1640 // Get the new query from the suggestion.
1641 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1642 if (newQuery != null) {
1643 // The suggestion rewrites the query.
1644 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
1645 // Update the text field, without getting new suggestions.
1646 setQuery(newQuery);
1647 } else {
1648 // The suggestion does not rewrite the query, restore the user's query.
1649 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
1650 restoreUserQuery();
1651 }
1652 } else {
1653 // We got a bad position, restore the user's query.
1654 Log.w(LOG_TAG, "Bad suggestion position: " + position);
1655 restoreUserQuery();
1656 }
1657 }
1658
1659 /**
1660 * Restores the query entered by the user if needed.
1661 */
1662 private void restoreUserQuery() {
1663 if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
1664 setQuery(mUserQuery);
1665 }
1666
1667 /**
1668 * Sets the text in the query box, without updating the suggestions.
1669 */
1670 private void setQuery(CharSequence query) {
1671 mSearchAutoComplete.setText(query, false);
1672 if (query != null) {
1673 mSearchAutoComplete.setSelection(query.length());
1674 }
1675 }
1676
1677 /**
1678 * Sets the text in the query box, updating the suggestions.
1679 */
1680 private void setUserQuery(String query) {
1681 if (query == null) {
1682 query = "";
1683 }
1684 mUserQuery = query;
1685 mSearchAutoComplete.setText(query);
1686 mSearchAutoComplete.setSelection(query.length());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001687 }
1688
1689 /**
1690 * Debugging Support
1691 */
1692
1693 /**
1694 * For debugging only, sample the millisecond clock and log it.
1695 * Uses AtomicLong so we can use in multiple threads
1696 */
1697 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
1698 private void dbgLogTiming(final String caller) {
1699 long millis = SystemClock.uptimeMillis();
1700 long oldTime = mLastLogTime.getAndSet(millis);
1701 long delta = millis - oldTime;
1702 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
1703 Log.d(LOG_TAG,report);
1704 }
1705}