blob: bfd9923712bb8377de15811ed10b3b1dd5a6d9ad [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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.content.ComponentName;
Karl Rosaend4c98c42009-06-09 17:05:54 +010023import android.content.ContentResolver;
24import android.content.ContentValues;
Bjorn Bringert444c7272009-07-06 21:32:50 +010025import android.content.Context;
26import android.content.Intent;
Karl Rosaen875d50a2009-04-23 19:00:21 -070027import android.content.pm.ActivityInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.content.pm.PackageManager;
Karl Rosaen98e333f2009-04-28 10:39:09 -070029import android.content.pm.ResolveInfo;
Bjorn Bringertc1f40962009-04-29 13:08:39 +010030import android.content.pm.PackageManager.NameNotFoundException;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031import android.content.res.Resources;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080032import android.database.Cursor;
Romain Guyb5537c42009-06-30 12:39:18 -070033import android.graphics.drawable.Animatable;
Bjorn Bringert444c7272009-07-06 21:32:50 +010034import android.graphics.drawable.Drawable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035import android.net.Uri;
36import android.os.Bundle;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080037import android.os.SystemClock;
38import android.server.search.SearchableInfo;
39import android.speech.RecognizerIntent;
40import android.text.Editable;
41import android.text.InputType;
42import android.text.TextUtils;
43import android.text.TextWatcher;
Satish Sampath662df0b2009-06-22 23:16:07 +010044import android.text.util.Regex;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080045import android.util.AttributeSet;
46import android.util.Log;
Mike LeBeaua97f4a12009-05-23 01:19:36 -050047import android.view.ContextThemeWrapper;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048import android.view.Gravity;
49import android.view.KeyEvent;
Karl Rosaen875d50a2009-04-23 19:00:21 -070050import android.view.MotionEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import android.view.View;
Karl Rosaen875d50a2009-04-23 19:00:21 -070052import android.view.ViewConfiguration;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053import android.view.ViewGroup;
54import android.view.Window;
55import android.view.WindowManager;
Satish Sampath662df0b2009-06-22 23:16:07 +010056import android.view.inputmethod.EditorInfo;
Dianne Hackborna8f556e2009-03-24 20:47:50 -070057import android.view.inputmethod.InputMethodManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080058import android.widget.AdapterView;
59import android.widget.AutoCompleteTextView;
60import android.widget.Button;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061import android.widget.ImageButton;
Mike LeBeau1fd73232009-04-27 19:12:05 -070062import android.widget.ImageView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063import android.widget.ListView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064import android.widget.TextView;
Bjorn Bringertc1f40962009-04-29 13:08:39 +010065import android.widget.AdapterView.OnItemClickListener;
66import android.widget.AdapterView.OnItemSelectedListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080067
Karl Rosaen875d50a2009-04-23 19:00:21 -070068import java.util.ArrayList;
69import java.util.WeakHashMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080070import java.util.concurrent.atomic.AtomicLong;
71
72/**
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +010073 * System search dialog. This is controlled by the
74 * SearchManagerService and runs in the system process.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080075 *
76 * @hide
77 */
78public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
79
80 // Debugging support
Karl Rosaen875d50a2009-04-23 19:00:21 -070081 private static final boolean DBG = false;
82 private static final String LOG_TAG = "SearchDialog";
83 private static final boolean DBG_LOG_TIMING = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080084
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080085 private static final String INSTANCE_KEY_COMPONENT = "comp";
86 private static final String INSTANCE_KEY_APPDATA = "data";
87 private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +010088 private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp";
89 private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
90 private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev";
91 private static final String INSTANCE_KEY_USER_QUERY = "uQry";
Mike LeBeau260dfb52009-07-15 15:20:14 -070092
93 // The extra key used in an intent to the speech recognizer for in-app voice search.
94 private static final String EXTRA_CALLING_PACKAGE = "calling_package";
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +010095
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;
Bjorn Bringert444c7272009-07-06 21:32:50 +010098
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080099 // views & widgets
100 private TextView mBadgeLabel;
Mike LeBeau1fd73232009-04-27 19:12:05 -0700101 private ImageView mAppIcon;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700102 private SearchAutoComplete mSearchAutoComplete;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800103 private Button mGoButton;
104 private ImageButton mVoiceButton;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700105 private View mSearchPlate;
Romain Guyf4f70462009-06-26 16:55:54 -0700106 private Drawable mWorkingSpinner;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800107
108 // interaction with searchable application
Karl Rosaen875d50a2009-04-23 19:00:21 -0700109 private SearchableInfo mSearchable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110 private ComponentName mLaunchComponent;
111 private Bundle mAppSearchData;
112 private boolean mGlobalSearchMode;
113 private Context mActivityContext;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700114
Mike LeBeaub3aab692009-04-30 02:09:09 -0700115 // Values we store to allow user to toggle between in-app search and global search.
116 private ComponentName mStoredComponentName;
117 private Bundle mStoredAppSearchData;
118
Karl Rosaen875d50a2009-04-23 19:00:21 -0700119 // stack of previous searchables, to support the BACK key after
120 // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.
121 // The top of the stack (= previous searchable) is the last element of the list,
122 // since adding and removing is efficient at the end of an ArrayList.
123 private ArrayList<ComponentName> mPreviousComponents;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800124
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800125 // For voice searching
126 private Intent mVoiceWebSearchIntent;
127 private Intent mVoiceAppSearchIntent;
128
129 // support for AutoCompleteTextView suggestions display
130 private SuggestionsAdapter mSuggestionsAdapter;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700131
132 // Whether to rewrite queries when selecting suggestions
Mike LeBeaucce7dbc2009-06-18 09:25:18 -0700133 private static final boolean REWRITE_QUERIES = true;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700134
135 // The query entered by the user. This is not changed when selecting a suggestion
136 // that modifies the contents of the text field. But if the user then edits
137 // the suggestion, the resulting string is saved.
138 private String mUserQuery;
139
140 // A weak map of drawables we've gotten from other packages, so we don't load them
141 // more than once.
142 private final WeakHashMap<String, Drawable> mOutsideDrawablesCache =
143 new WeakHashMap<String, Drawable>();
Satish Sampath662df0b2009-06-22 23:16:07 +0100144
145 // Last known IME options value for the search edit text.
146 private int mSearchAutoCompleteImeOptions;
147
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800148 /**
149 * Constructor - fires it up and makes it look like the search UI.
150 *
151 * @param context Application Context we can use for system acess
152 */
153 public SearchDialog(Context context) {
Mike LeBeaua97f4a12009-05-23 01:19:36 -0500154 super(context, com.android.internal.R.style.Theme_GlobalSearchBar);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800155 }
156
157 /**
158 * We create the search dialog just once, and it stays around (hidden)
159 * until activated by the user.
160 */
161 @Override
162 protected void onCreate(Bundle savedInstanceState) {
163 super.onCreate(savedInstanceState);
164
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800165 setContentView(com.android.internal.R.layout.search_bar);
166
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100167 Window theWindow = getWindow();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800168 WindowManager.LayoutParams lp = theWindow.getAttributes();
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100169 lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR;
170 lp.width = ViewGroup.LayoutParams.FILL_PARENT;
171 // taking up the whole window (even when transparent) is less than ideal,
172 // but necessary to show the popup window until the window manager supports
173 // having windows anchored by their parent but not clipped by them.
174 lp.height = ViewGroup.LayoutParams.FILL_PARENT;
175 lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
Mike LeBeau98acd542009-05-07 19:04:39 -0700176 lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800177 theWindow.setAttributes(lp);
178
179 // get the view elements for local access
180 mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700181 mSearchAutoComplete = (SearchAutoComplete)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800182 findViewById(com.android.internal.R.id.search_src_text);
Mike LeBeau1fd73232009-04-27 19:12:05 -0700183 mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800184 mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
185 mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700186 mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
Romain Guyf4f70462009-06-26 16:55:54 -0700187 mWorkingSpinner = getContext().getResources().
Mike LeBeau1480eb22009-05-20 17:22:13 -0700188 getDrawable(com.android.internal.R.drawable.search_spinner);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800189
190 // attach listeners
Karl Rosaen875d50a2009-04-23 19:00:21 -0700191 mSearchAutoComplete.addTextChangedListener(mTextWatcher);
192 mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
193 mSearchAutoComplete.setOnItemClickListener(this);
194 mSearchAutoComplete.setOnItemSelectedListener(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800195 mGoButton.setOnClickListener(mGoButtonClickListener);
196 mGoButton.setOnKeyListener(mButtonsKeyListener);
197 mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
198 mVoiceButton.setOnKeyListener(mButtonsKeyListener);
199
Karl Rosaen875d50a2009-04-23 19:00:21 -0700200 mSearchAutoComplete.setSearchDialog(this);
201
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800202 // pre-hide all the extraneous elements
203 mBadgeLabel.setVisibility(View.GONE);
204
205 // Additional adjustments to make Dialog work for Search
206
207 // Touching outside of the search dialog will dismiss it
208 setCanceledOnTouchOutside(true);
Bjorn Bringert444c7272009-07-06 21:32:50 +0100209
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800210 // Save voice intent for later queries/launching
211 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100212 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800213 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
214 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
215
216 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100217 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Satish Sampath662df0b2009-06-22 23:16:07 +0100218
219 mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800220 }
221
222 /**
223 * Set up the search dialog
224 *
Karl Rosaen875d50a2009-04-23 19:00:21 -0700225 * @return true if search dialog launched, false if not
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 */
227 public boolean show(String initialQuery, boolean selectInitialQuery,
228 ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100229
Mike LeBeaub3aab692009-04-30 02:09:09 -0700230 // Reset any stored values from last time dialog was shown.
231 mStoredComponentName = null;
232 mStoredAppSearchData = null;
Satish Sampathfef8d3e2009-07-01 17:48:42 +0100233
234 boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData,
235 globalSearch);
236 if (success) {
237 // Display the drop down as soon as possible instead of waiting for the rest of the
238 // pending UI stuff to get done, so that things appear faster to the user.
239 mSearchAutoComplete.showDropDownAfterLayout();
240 }
241 return success;
Mike LeBeaub3aab692009-04-30 02:09:09 -0700242 }
243
Mike LeBeaub3aab692009-04-30 02:09:09 -0700244 /**
245 * Called in response to a press of the hard search button in
246 * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app
247 * search and global search when relevant.
248 *
249 * If pressed within an in-app search context, this switches the search dialog out to
250 * global search. If pressed within a global search context that was originally an in-app
251 * search context, this switches back to the in-app search context. If pressed within a
252 * global search context that has no original in-app search context (e.g., global search
253 * from Home), this does nothing.
254 *
255 * @return false if we wanted to toggle context but could not do so successfully, true
256 * in all other cases
257 */
258 private boolean toggleGlobalSearch() {
259 String currentSearchText = mSearchAutoComplete.getText().toString();
260 if (!mGlobalSearchMode) {
261 mStoredComponentName = mLaunchComponent;
262 mStoredAppSearchData = mAppSearchData;
263 return doShow(currentSearchText, false, null, mAppSearchData, true);
264 } else {
265 if (mStoredComponentName != null) {
266 // This means we should toggle *back* to an in-app search context from
267 // global search.
268 return doShow(currentSearchText, false, mStoredComponentName,
269 mStoredAppSearchData, false);
270 } else {
271 return true;
272 }
273 }
274 }
275
276 /**
277 * Does the rest of the work required to show the search dialog. Called by both
278 * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and
279 * {@link #toggleGlobalSearch()}.
280 *
281 * @return true if search dialog showed, false if not
282 */
283 private boolean doShow(String initialQuery, boolean selectInitialQuery,
284 ComponentName componentName, Bundle appSearchData,
285 boolean globalSearch) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700286 // set up the searchable and show the dialog
287 if (!show(componentName, appSearchData, globalSearch)) {
288 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700290
Karl Rosaen875d50a2009-04-23 19:00:21 -0700291 // finally, load the user's initial text (which may trigger suggestions)
292 setUserQuery(initialQuery);
293 if (selectInitialQuery) {
294 mSearchAutoComplete.selectAll();
295 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700296
Karl Rosaen875d50a2009-04-23 19:00:21 -0700297 return true;
298 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700299
Karl Rosaen875d50a2009-04-23 19:00:21 -0700300 /**
301 * Sets up the search dialog and shows it.
302 *
303 * @return <code>true</code> if search dialog launched
304 */
305 private boolean show(ComponentName componentName, Bundle appSearchData,
306 boolean globalSearch) {
307
308 if (DBG) {
309 Log.d(LOG_TAG, "show(" + componentName + ", "
310 + appSearchData + ", " + globalSearch + ")");
311 }
312
Bjorn Bringert8d153822009-06-22 10:31:44 +0100313 SearchManager searchManager = (SearchManager)
314 mContext.getSystemService(Context.SEARCH_SERVICE);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700315 // Try to get the searchable info for the provided component (or for global search,
316 // if globalSearch == true).
Bjorn Bringert8d153822009-06-22 10:31:44 +0100317 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700318
319 // If we got back nothing, and it wasn't a request for global search, then try again
320 // for global search, as we'll try to launch that in lieu of any component-specific search.
321 if (!globalSearch && mSearchable == null) {
322 globalSearch = true;
Bjorn Bringert8d153822009-06-22 10:31:44 +0100323 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800324 }
Bjorn Bringertee716fa2009-07-16 09:15:37 +0100325
326 // If there's not even a searchable info available for global search, then really give up.
327 if (mSearchable == null) {
328 Log.w(LOG_TAG, "No global search provider.");
329 return false;
330 }
331
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800332 mLaunchComponent = componentName;
333 mAppSearchData = appSearchData;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700334 // Using globalSearch here is just an optimization, just calling
335 // isDefaultSearchable() should always give the same result.
Bjorn Bringert8d153822009-06-22 10:31:44 +0100336 mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700337 mActivityContext = mSearchable.getActivityContext(getContext());
338
339 // show the dialog. this will call onStart().
Mike LeBeau1fffbd92009-07-14 15:57:41 -0700340 if (!isShowing()) {
Mike LeBeaua97f4a12009-05-23 01:19:36 -0500341 // The Dialog uses a ContextThemeWrapper for the context; use this to change the
342 // theme out from underneath us, between the global search theme and the in-app
343 // search theme. They are identical except that the global search theme does not
344 // dim the background of the window (because global search is full screen so it's
345 // not needed and this should save a little bit of time on global search invocation).
346 Object context = getContext();
347 if (context instanceof ContextThemeWrapper) {
348 ContextThemeWrapper wrapper = (ContextThemeWrapper) context;
349 if (globalSearch) {
350 wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar);
351 } else {
352 wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar);
353 }
354 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700355 show();
356 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800357
Karl Rosaen875d50a2009-04-23 19:00:21 -0700358 updateUI();
359
360 return true;
361 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800362
363 /**
364 * The search dialog is being dismissed, so handle all of the local shutdown operations.
365 *
366 * This function is designed to be idempotent so that dismiss() can be safely called at any time
367 * (even if already closed) and more likely to really dump any memory. No leaks!
368 */
369 @Override
370 public void onStop() {
371 super.onStop();
Bjorn Bringert444c7272009-07-06 21:32:50 +0100372
Karl Rosaen875d50a2009-04-23 19:00:21 -0700373 closeSuggestionsAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800374
375 // dump extra memory we're hanging on to
376 mLaunchComponent = null;
377 mAppSearchData = null;
378 mSearchable = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800379 mActivityContext = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800380 mUserQuery = null;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700381 mPreviousComponents = null;
382 }
Mike LeBeau1c690752009-05-20 20:20:26 -0700383
Mike LeBeau1c690752009-05-20 20:20:26 -0700384 /**
Mike LeBeau1480eb22009-05-20 17:22:13 -0700385 * Sets the search dialog to the 'working' state, which shows a working spinner in the
386 * right hand size of the text field.
387 *
388 * @param working true to show spinner, false to hide spinner
389 */
390 public void setWorking(boolean working) {
391 if (working) {
392 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
393 null, null, mWorkingSpinner, null);
Romain Guyb5537c42009-06-30 12:39:18 -0700394 ((Animatable) mWorkingSpinner).start();
Mike LeBeau1480eb22009-05-20 17:22:13 -0700395 } else {
396 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
397 null, null, null, null);
Romain Guyb5537c42009-06-30 12:39:18 -0700398 ((Animatable) mWorkingSpinner).stop();
Mike LeBeau1480eb22009-05-20 17:22:13 -0700399 }
400 }
401
402 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700403 * Closes and gets rid of the suggestions adapter.
404 */
405 private void closeSuggestionsAdapter() {
406 // remove the adapter from the autocomplete first, to avoid any updates
407 // when we drop the cursor
408 mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
409 // close any leftover cursor
410 if (mSuggestionsAdapter != null) {
411 mSuggestionsAdapter.changeCursor(null);
412 }
413 mSuggestionsAdapter = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800414 }
415
416 /**
417 * Save the minimal set of data necessary to recreate the search
418 *
Bjorn Bringert444c7272009-07-06 21:32:50 +0100419 * @return A bundle with the state of the dialog, or {@code null} if the search
420 * dialog is not showing.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800421 */
422 @Override
423 public Bundle onSaveInstanceState() {
Bjorn Bringert444c7272009-07-06 21:32:50 +0100424 if (!isShowing()) return null;
425
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800426 Bundle bundle = new Bundle();
Bjorn Bringert444c7272009-07-06 21:32:50 +0100427
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800428 // setup info so I can recreate this particular search
429 bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
430 bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
431 bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +0100432 bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName);
433 bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData);
434 bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents);
435 bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
436
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800437 return bundle;
438 }
439
440 /**
441 * Restore the state of the dialog from a previously saved bundle.
Karl Rosaen875d50a2009-04-23 19:00:21 -0700442 *
443 * TODO: go through this and make sure that it saves everything that is saved
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800444 *
445 * @param savedInstanceState The state of the dialog previously saved by
446 * {@link #onSaveInstanceState()}.
447 */
448 @Override
449 public void onRestoreInstanceState(Bundle savedInstanceState) {
Bjorn Bringert444c7272009-07-06 21:32:50 +0100450 if (savedInstanceState == null) return;
451
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800452 ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
453 Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
454 boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +0100455 ComponentName storedComponentName =
456 savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT);
457 Bundle storedAppSearchData =
458 savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA);
459 ArrayList<ComponentName> previousComponents =
460 savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS);
461 String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
462
463 // Set stored state
464 mStoredComponentName = storedComponentName;
465 mStoredAppSearchData = storedAppSearchData;
466 mPreviousComponents = previousComponents;
467
468 // show the dialog.
469 if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800470 // for some reason, we couldn't re-instantiate
471 return;
472 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800473 }
474
475 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700476 * Called after resources have changed, e.g. after screen rotation or locale change.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800477 */
Bjorn Bringert444c7272009-07-06 21:32:50 +0100478 public void onConfigurationChanged() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800479 if (isShowing()) {
480 // Redraw (resources may have changed)
481 updateSearchButton();
Mike LeBeau1fd73232009-04-27 19:12:05 -0700482 updateSearchAppIcon();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800483 updateSearchBadge();
484 updateQueryHint();
485 }
486 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700487
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700489 * Update the UI according to the info in the current value of {@link #mSearchable}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800490 */
Karl Rosaen875d50a2009-04-23 19:00:21 -0700491 private void updateUI() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800492 if (mSearchable != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700493 updateSearchAutoComplete();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800494 updateSearchButton();
Mike LeBeau1fd73232009-04-27 19:12:05 -0700495 updateSearchAppIcon();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800496 updateSearchBadge();
497 updateQueryHint();
498 updateVoiceButton();
499
500 // In order to properly configure the input method (if one is being used), we
501 // need to let it know if we'll be providing suggestions. Although it would be
502 // difficult/expensive to know if every last detail has been configured properly, we
503 // can at least see if a suggestions provider has been configured, and use that
504 // as our trigger.
505 int inputType = mSearchable.getInputType();
506 // We only touch this if the input type is set up for text (which it almost certainly
507 // should be, in the case of search!)
508 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
509 // The existence of a suggestions authority is the proxy for "suggestions
510 // are available here"
511 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
512 if (mSearchable.getSuggestAuthority() != null) {
513 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
514 }
515 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700516 mSearchAutoComplete.setInputType(inputType);
Satish Sampath662df0b2009-06-22 23:16:07 +0100517 mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
518 mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700519 }
520 }
521
522 /**
523 * Updates the auto-complete text view.
524 */
525 private void updateSearchAutoComplete() {
526 // close any existing suggestions adapter
527 closeSuggestionsAdapter();
528
529 mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
Bjorn Bringert203464a2009-04-27 17:08:11 +0100530 mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100531 // we dismiss the entire dialog instead
532 mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700533
534 if (mGlobalSearchMode) {
535 mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in
Karl Rosaen875d50a2009-04-23 19:00:21 -0700536 } else {
537 mSearchAutoComplete.setDropDownAlwaysVisible(false);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700538 }
539
540 // attach the suggestions adapter, if suggestions are available
541 // The existence of a suggestions authority is the proxy for "suggestions available here"
542 if (mSearchable.getSuggestAuthority() != null) {
Mike LeBeau1480eb22009-05-20 17:22:13 -0700543 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable,
544 mOutsideDrawablesCache, mGlobalSearchMode);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700545 mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800546 }
547 }
548
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800549 /**
550 * Update the text in the search button. Note: This is deprecated functionality, for
551 * 1.0 compatibility only.
552 */
553 private void updateSearchButton() {
554 String textLabel = null;
555 Drawable iconLabel = null;
556 int textId = mSearchable.getSearchButtonText();
557 if (textId != 0) {
558 textLabel = mActivityContext.getResources().getString(textId);
559 } else {
560 iconLabel = getContext().getResources().
561 getDrawable(com.android.internal.R.drawable.ic_btn_search);
562 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700563 mGoButton.setText(textLabel);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800564 mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
565 }
566
Mike LeBeau1fd73232009-04-27 19:12:05 -0700567 private void updateSearchAppIcon() {
568 if (mGlobalSearchMode) {
569 mAppIcon.setImageResource(0);
570 mAppIcon.setVisibility(View.GONE);
571 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
572 mSearchPlate.getPaddingTop(),
573 mSearchPlate.getPaddingRight(),
574 mSearchPlate.getPaddingBottom());
575 } else {
576 PackageManager pm = getContext().getPackageManager();
Romain Guyf4f70462009-06-26 16:55:54 -0700577 Drawable icon;
Mike LeBeau1fd73232009-04-27 19:12:05 -0700578 try {
579 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
580 icon = pm.getApplicationIcon(info.applicationInfo);
581 if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
582 } catch (NameNotFoundException e) {
583 icon = pm.getDefaultActivityIcon();
584 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
585 }
586 mAppIcon.setImageDrawable(icon);
587 mAppIcon.setVisibility(View.VISIBLE);
588 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
589 mSearchPlate.getPaddingTop(),
590 mSearchPlate.getPaddingRight(),
591 mSearchPlate.getPaddingBottom());
592 }
593 }
594
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800595 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700596 * Setup the search "Badge" if requested by mode flags.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800597 */
598 private void updateSearchBadge() {
599 // assume both hidden
600 int visibility = View.GONE;
601 Drawable icon = null;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700602 CharSequence text = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800603
604 // optionally show one or the other.
Bjorn Bringerta9204132009-05-05 14:06:35 +0100605 if (mSearchable.useBadgeIcon()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800606 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
607 visibility = View.VISIBLE;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700608 if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
Bjorn Bringerta9204132009-05-05 14:06:35 +0100609 } else if (mSearchable.useBadgeLabel()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800610 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
611 visibility = View.VISIBLE;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700612 if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800613 }
614
615 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
616 mBadgeLabel.setText(text);
617 mBadgeLabel.setVisibility(visibility);
618 }
619
620 /**
621 * Update the hint in the query text field.
622 */
623 private void updateQueryHint() {
624 if (isShowing()) {
625 String hint = null;
626 if (mSearchable != null) {
627 int hintId = mSearchable.getHintId();
628 if (hintId != 0) {
629 hint = mActivityContext.getString(hintId);
630 }
631 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700632 mSearchAutoComplete.setHint(hint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800633 }
634 }
635
636 /**
637 * Update the visibility of the voice button. There are actually two voice search modes,
638 * either of which will activate the button.
639 */
640 private void updateVoiceButton() {
641 int visibility = View.GONE;
642 if (mSearchable.getVoiceSearchEnabled()) {
643 Intent testIntent = null;
644 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
645 testIntent = mVoiceWebSearchIntent;
646 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
647 testIntent = mVoiceAppSearchIntent;
648 }
649 if (testIntent != null) {
650 ResolveInfo ri = getContext().getPackageManager().
651 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
652 if (ri != null) {
653 visibility = View.VISIBLE;
654 }
655 }
656 }
657 mVoiceButton.setVisibility(visibility);
658 }
659
660 /**
661 * Listeners of various types
662 */
663
664 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700665 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
666 * touch is outside the window. But the window includes space for the drop-down,
667 * so we also cancel on taps outside the search bar when the drop-down is not showing.
668 */
669 @Override
670 public boolean onTouchEvent(MotionEvent event) {
671 // cancel if the drop-down is not showing and the touch event was outside the search plate
672 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
673 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
674 cancel();
675 return true;
676 }
677 // Let Dialog handle events outside the window while the pop-up is showing.
678 return super.onTouchEvent(event);
679 }
680
681 private boolean isOutOfBounds(View v, MotionEvent event) {
682 final int x = (int) event.getX();
683 final int y = (int) event.getY();
684 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
685 return (x < -slop) || (y < -slop)
686 || (x > (v.getWidth()+slop))
687 || (y > (v.getHeight()+slop));
688 }
689
690 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800691 * Dialog's OnKeyListener implements various search-specific functionality
692 *
693 * @param keyCode This is the keycode of the typed key, and is the same value as
Karl Rosaen875d50a2009-04-23 19:00:21 -0700694 * found in the KeyEvent parameter.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800695 * @param event The complete event record for the typed key
696 *
697 * @return Return true if the event was handled here, or false if not.
698 */
699 @Override
700 public boolean onKeyDown(int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700701 if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
Bjorn Bringertee716fa2009-07-16 09:15:37 +0100702 if (mSearchable == null) {
703 return false;
704 }
705
Karl Rosaen875d50a2009-04-23 19:00:21 -0700706 // handle back key to go back to previous searchable, etc.
707 if (handleBackKey(keyCode, event)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800708 return true;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700709 }
710
Karl Rosaen875d50a2009-04-23 19:00:21 -0700711 if (keyCode == KeyEvent.KEYCODE_SEARCH) {
Mike LeBeaub3aab692009-04-30 02:09:09 -0700712 // If the search key is pressed, toggle between global and in-app search. If we are
713 // currently doing global search and there is no in-app search context to toggle to,
714 // just don't do anything.
715 return toggleGlobalSearch();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800716 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700717
718 // if it's an action specified by the searchable activity, launch the
719 // entered query with the action key
720 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
Bjorn Bringerta9204132009-05-05 14:06:35 +0100721 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
722 launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
Karl Rosaen875d50a2009-04-23 19:00:21 -0700723 return true;
724 }
725
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800726 return false;
727 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700728
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800729 /**
730 * Callback to watch the textedit field for empty/non-empty
731 */
732 private TextWatcher mTextWatcher = new TextWatcher() {
733
Karl Rosaen875d50a2009-04-23 19:00:21 -0700734 public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800735
736 public void onTextChanged(CharSequence s, int start,
737 int before, int after) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700738 if (DBG_LOG_TIMING) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800739 dbgLogTiming("onTextChanged()");
740 }
Bjorn Bringertee716fa2009-07-16 09:15:37 +0100741 if (mSearchable == null) {
742 return;
743 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800744 updateWidgetState();
Karl Rosaen875d50a2009-04-23 19:00:21 -0700745 if (!mSearchAutoComplete.isPerformingCompletion()) {
746 // The user changed the query, remember it.
747 mUserQuery = s == null ? "" : s.toString();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800748 }
749 }
750
Satish Sampath662df0b2009-06-22 23:16:07 +0100751 public void afterTextChanged(Editable s) {
Dianne Hackbornb06ea702009-07-13 13:07:51 -0700752 if (mSearchable == null) {
753 return;
754 }
Satish Sampathd21572c2009-07-08 14:54:11 +0100755 if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) {
Satish Sampath662df0b2009-06-22 23:16:07 +0100756 // The user changed the query, check if it is a URL and if so change the search
757 // button in the soft keyboard to the 'Go' button.
758 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION));
759 if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) {
760 options = options | EditorInfo.IME_ACTION_GO;
761 } else {
762 options = options | EditorInfo.IME_ACTION_SEARCH;
763 }
764 if (options != mSearchAutoCompleteImeOptions) {
765 mSearchAutoCompleteImeOptions = options;
766 mSearchAutoComplete.setImeOptions(options);
767 // This call is required to update the soft keyboard UI with latest IME flags.
768 mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
769 }
770 }
771 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800772 };
773
774 /**
775 * Enable/Disable the cancel button based on edit text state (any text?)
776 */
777 private void updateWidgetState() {
778 // enable the button if we have one or more non-space characters
Karl Rosaen875d50a2009-04-23 19:00:21 -0700779 boolean enabled = !mSearchAutoComplete.isEmpty();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800780 mGoButton.setEnabled(enabled);
781 mGoButton.setFocusable(enabled);
782 }
783
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800784 /**
785 * React to typing in the GO search button by refocusing to EditText.
786 * Continue typing the query.
787 */
788 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
789 public boolean onKey(View v, int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700790 // guard against possible race conditions
791 if (mSearchable == null) {
792 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800793 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700794
795 if (!event.isSystem() &&
796 (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
797 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
798 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
799 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
800 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
801 // restore focus and give key to EditText ...
802 if (mSearchAutoComplete.requestFocus()) {
803 return mSearchAutoComplete.dispatchKeyEvent(event);
804 }
805 }
806
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800807 return false;
808 }
809 };
810
811 /**
812 * React to a click in the GO button by launching a search.
813 */
814 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
815 public void onClick(View v) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700816 // guard against possible race conditions
817 if (mSearchable == null) {
818 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800819 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700820 launchQuerySearch();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800821 }
822 };
823
824 /**
825 * React to a click in the voice search button.
826 */
827 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
828 public void onClick(View v) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700829 // guard against possible race conditions
830 if (mSearchable == null) {
831 return;
832 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800833 try {
834 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
835 getContext().startActivity(mVoiceWebSearchIntent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800836 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
837 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
838 getContext().startActivity(appSearchIntent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800839 }
840 } catch (ActivityNotFoundException e) {
841 // Should not happen, since we check the availability of
842 // voice search before showing the button. But just in case...
843 Log.w(LOG_TAG, "Could not find voice search activity");
844 }
845 }
846 };
847
848 /**
849 * Create and return an Intent that can launch the voice search activity, perform a specific
850 * voice transcription, and forward the results to the searchable activity.
851 *
852 * @param baseIntent The voice app search intent to start from
853 * @return A completely-configured intent ready to send to the voice search activity
854 */
855 private Intent createVoiceAppSearchIntent(Intent baseIntent) {
Mike LeBeau260dfb52009-07-15 15:20:14 -0700856 ComponentName searchActivity = mSearchable.getSearchActivity();
857
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800858 // create the necessary intent to set up a search-and-forward operation
859 // in the voice search system. We have to keep the bundle separate,
860 // because it becomes immutable once it enters the PendingIntent
861 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
Mike LeBeau260dfb52009-07-15 15:20:14 -0700862 queryIntent.setComponent(searchActivity);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800863 PendingIntent pending = PendingIntent.getActivity(
864 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
865
866 // Now set up the bundle that will be inserted into the pending intent
867 // when it's time to do the search. We always build it here (even if empty)
868 // because the voice search activity will always need to insert "QUERY" into
869 // it anyway.
870 Bundle queryExtras = new Bundle();
871 if (mAppSearchData != null) {
872 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
873 }
874
875 // Now build the intent to launch the voice search. Add all necessary
876 // extras to launch the voice recognizer, and then all the necessary extras
877 // to forward the results to the searchable activity
878 Intent voiceIntent = new Intent(baseIntent);
879
880 // Add all of the configuration options supplied by the searchable's metadata
881 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
882 String prompt = null;
883 String language = null;
884 int maxResults = 1;
885 Resources resources = mActivityContext.getResources();
886 if (mSearchable.getVoiceLanguageModeId() != 0) {
887 languageModel = resources.getString(mSearchable.getVoiceLanguageModeId());
888 }
889 if (mSearchable.getVoicePromptTextId() != 0) {
890 prompt = resources.getString(mSearchable.getVoicePromptTextId());
891 }
892 if (mSearchable.getVoiceLanguageId() != 0) {
893 language = resources.getString(mSearchable.getVoiceLanguageId());
894 }
895 if (mSearchable.getVoiceMaxResults() != 0) {
896 maxResults = mSearchable.getVoiceMaxResults();
897 }
898 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
899 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
900 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
901 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
Mike LeBeau260dfb52009-07-15 15:20:14 -0700902 voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
903 searchActivity == null ? null : searchActivity.toShortString());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800904
905 // Add the values that configure forwarding the results
906 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
907 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
908
909 return voiceIntent;
910 }
911
912 /**
Satish Sampath662df0b2009-06-22 23:16:07 +0100913 * Corrects http/https typo errors in the given url string, and if the protocol specifier was
914 * not present defaults to http.
915 *
916 * @param inUrl URL to check and fix
917 * @return fixed URL string.
918 */
919 private String fixUrl(String inUrl) {
920 if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
921 return inUrl;
922
923 if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
924 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
925 inUrl = inUrl.replaceFirst("/", "//");
926 } else {
927 inUrl = inUrl.replaceFirst(":", "://");
928 }
929 }
930
931 if (inUrl.indexOf("://") == -1) {
932 inUrl = "http://" + inUrl;
933 }
934
935 return inUrl;
936 }
937
938 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800939 * React to the user typing "enter" or other hardwired keys while typing in the search box.
940 * This handles these special keys while the edit box has focus.
941 */
942 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
943 public boolean onKey(View v, int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700944 // guard against possible race conditions
945 if (mSearchable == null) {
946 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800947 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700948
949 if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
950 if (DBG) {
951 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
952 + "), selection: " + mSearchAutoComplete.getListSelection());
953 }
954
955 // If a suggestion is selected, handle enter, search key, and action keys
956 // as presses on the selected suggestion
957 if (mSearchAutoComplete.isPopupShowing() &&
958 mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
959 return onSuggestionsKey(v, keyCode, event);
960 }
961
962 // If there is text in the query box, handle enter, and action keys
963 // The search key is handled by the dialog's onKeyDown().
964 if (!mSearchAutoComplete.isEmpty()) {
965 if (keyCode == KeyEvent.KEYCODE_ENTER
966 && event.getAction() == KeyEvent.ACTION_UP) {
967 v.cancelLongPress();
Satish Sampath662df0b2009-06-22 23:16:07 +0100968
Satish Sampathb1665f22009-07-10 15:43:38 +0100969 // If this is a url entered by the user & we displayed the 'Go' button which
970 // the user clicked, launch the url instead of using it as a search query.
971 if (mSearchable.autoUrlDetect() &&
972 (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
Satish Sampathd21572c2009-07-08 14:54:11 +0100973 == EditorInfo.IME_ACTION_GO) {
Satish Sampathb1665f22009-07-10 15:43:38 +0100974 Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
975 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
976 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
977 launchIntent(intent);
978 } else {
979 // Launch as a regular search.
980 launchQuerySearch();
Satish Sampath662df0b2009-06-22 23:16:07 +0100981 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700982 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800983 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700984 if (event.getAction() == KeyEvent.ACTION_DOWN) {
985 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
Bjorn Bringerta9204132009-05-05 14:06:35 +0100986 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
987 launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800988 return true;
989 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800990 }
991 }
992 return false;
993 }
994 };
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800995
Dianne Hackborna8f556e2009-03-24 20:47:50 -0700996 @Override
Bjorn Bringertec8ee342009-07-08 21:49:42 +0100997 public void dismiss() {
Bjorn Bringert444c7272009-07-06 21:32:50 +0100998 if (!isShowing()) return;
999
Dianne Hackborna8f556e2009-03-24 20:47:50 -07001000 // We made sure the IME was displayed, so also make sure it is closed
1001 // when we go away.
1002 InputMethodManager imm = (InputMethodManager)getContext()
1003 .getSystemService(Context.INPUT_METHOD_SERVICE);
1004 if (imm != null) {
1005 imm.hideSoftInputFromWindow(
1006 getWindow().getDecorView().getWindowToken(), 0);
1007 }
1008
Bjorn Bringertec8ee342009-07-08 21:49:42 +01001009 super.dismiss();
Dianne Hackborna8f556e2009-03-24 20:47:50 -07001010 }
1011
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001012 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001013 * React to the user typing while in the suggestions list. First, check for action
1014 * keys. If not handled, try refocusing regular characters into the EditText.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001015 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001016 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1017 // guard against possible race conditions (late arrival after dismiss)
1018 if (mSearchable == null) {
1019 return false;
1020 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001021 if (mSuggestionsAdapter == null) {
1022 return false;
1023 }
1024 if (event.getAction() == KeyEvent.ACTION_DOWN) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001025 if (DBG_LOG_TIMING) {
1026 dbgLogTiming("onSuggestionsKey()");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001027 }
1028
1029 // First, check for enter or search (both of which we'll treat as a "click")
1030 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001031 int position = mSearchAutoComplete.getListSelection();
1032 return launchSuggestion(position);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001033 }
1034
1035 // Next, check for left/right moves, which we use to "return" the user to the edit view
1036 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001037 // give "focus" to text editor, with cursor at the beginning if
1038 // left key, at end if right key
1039 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001040 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
Karl Rosaen875d50a2009-04-23 19:00:21 -07001041 0 : mSearchAutoComplete.length();
1042 mSearchAutoComplete.setSelection(selPoint);
1043 mSearchAutoComplete.setListSelection(0);
1044 mSearchAutoComplete.clearListSelection();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001045 return true;
1046 }
1047
1048 // Next, check for an "up and out" move
Karl Rosaen875d50a2009-04-23 19:00:21 -07001049 if (keyCode == KeyEvent.KEYCODE_DPAD_UP
1050 && 0 == mSearchAutoComplete.getListSelection()) {
1051 restoreUserQuery();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001052 // let ACTV complete the move
1053 return false;
1054 }
1055
1056 // Next, check for an "action key"
1057 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1058 if ((actionKey != null) &&
Bjorn Bringerta9204132009-05-05 14:06:35 +01001059 ((actionKey.getSuggestActionMsg() != null) ||
1060 (actionKey.getSuggestActionMsgColumn() != null))) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001061 // launch suggestion using action key column
1062 int position = mSearchAutoComplete.getListSelection();
1063 if (position != ListView.INVALID_POSITION) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001064 Cursor c = mSuggestionsAdapter.getCursor();
1065 if (c.moveToPosition(position)) {
1066 final String actionMsg = getActionKeyMessage(c, actionKey);
1067 if (actionMsg != null && (actionMsg.length() > 0)) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001068 return launchSuggestion(position, keyCode, actionMsg);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001069 }
1070 }
1071 }
1072 }
1073 }
1074 return false;
Karl Rosaen875d50a2009-04-23 19:00:21 -07001075 }
1076
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001077 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001078 * Launch a search for the text in the query text field.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001079 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001080 protected void launchQuerySearch() {
1081 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001082 }
1083
1084 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001085 * Launch a search for the text in the query text field.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001086 *
Karl Rosaen875d50a2009-04-23 19:00:21 -07001087 * @param actionKey The key code of the action key that was pressed,
1088 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1089 * @param actionMsg The message for the action key that was pressed,
1090 * or <code>null</code> if none.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001091 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001092 protected void launchQuerySearch(int actionKey, String actionMsg) {
1093 String query = mSearchAutoComplete.getText().toString();
Satish Sampathbf23fe02009-06-15 23:47:56 +01001094 Intent intent = createIntent(Intent.ACTION_SEARCH, null, null, query, null,
Karl Rosaen875d50a2009-04-23 19:00:21 -07001095 actionKey, actionMsg);
1096 launchIntent(intent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001097 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001098
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001099 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001100 * Launches an intent based on a suggestion.
1101 *
1102 * @param position The index of the suggestion to create the intent from.
1103 * @return true if a successful launch, false if could not (e.g. bad position).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001104 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001105 protected boolean launchSuggestion(int position) {
1106 return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1107 }
1108
1109 /**
1110 * Launches an intent based on a suggestion.
1111 *
1112 * @param position The index of the suggestion to create the intent from.
1113 * @param actionKey The key code of the action key that was pressed,
1114 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1115 * @param actionMsg The message for the action key that was pressed,
1116 * or <code>null</code> if none.
1117 * @return true if a successful launch, false if could not (e.g. bad position).
1118 */
1119 protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1120 Cursor c = mSuggestionsAdapter.getCursor();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001121 if ((c != null) && c.moveToPosition(position)) {
Karl Rosaend4c98c42009-06-09 17:05:54 +01001122
1123 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1124
1125 // report back about the click
1126 if (mGlobalSearchMode) {
1127 // in global search mode, do it via cursor
1128 mSuggestionsAdapter.callCursorOnClick(c, position);
1129 } else if (intent != null
1130 && mPreviousComponents != null
1131 && !mPreviousComponents.isEmpty()) {
1132 // in-app search (and we have pivoted in as told by mPreviousComponents,
1133 // which is used for keeping track of what we pop back to when we are pivoting into
1134 // in app search.)
1135 reportInAppClickToGlobalSearch(c, intent);
1136 }
Karl Rosaena058f022009-06-01 23:11:44 +01001137
1138 // launch the intent
Karl Rosaen875d50a2009-04-23 19:00:21 -07001139 launchIntent(intent);
Karl Rosaena058f022009-06-01 23:11:44 +01001140
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001141 return true;
1142 }
1143 return false;
1144 }
Karl Rosaena058f022009-06-01 23:11:44 +01001145
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001146 /**
Karl Rosaend4c98c42009-06-09 17:05:54 +01001147 * Report a click from an in app search result back to global search for shortcutting porpoises.
1148 *
1149 * @param c The cursor that is pointing to the clicked position.
1150 * @param intent The intent that will be launched for the click.
1151 */
1152 private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) {
1153 // for in app search, still tell global search via content provider
1154 Uri uri = getClickReportingUri();
1155 final ContentValues cv = new ContentValues();
1156 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery);
1157 final ComponentName source = mSearchable.getSearchActivity();
1158 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString());
1159
1160 // grab the intent columns from the intent we created since it has additional
1161 // logic for falling back on the searchable default
1162 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction());
1163 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString());
Satish Sampathbf23fe02009-06-15 23:47:56 +01001164 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME,
1165 intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY));
Karl Rosaend4c98c42009-06-09 17:05:54 +01001166
1167 // ensure the icons will work for global search
1168 cv.put(SearchManager.SUGGEST_COLUMN_ICON_1,
1169 wrapIconForPackage(
1170 source,
1171 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1)));
1172 cv.put(SearchManager.SUGGEST_COLUMN_ICON_2,
1173 wrapIconForPackage(
1174 source,
1175 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2)));
1176
1177 // the rest can be passed through directly
1178 cv.put(SearchManager.SUGGEST_COLUMN_FORMAT,
1179 getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT));
1180 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
1181 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1));
1182 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
1183 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2));
1184 cv.put(SearchManager.SUGGEST_COLUMN_QUERY,
1185 getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY));
1186 cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
1187 getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID));
1188 // note: deliberately omitting background color since it is only for global search
1189 // "more results" entries
1190 mContext.getContentResolver().insert(uri, cv);
1191 }
1192
1193 /**
1194 * @return A URI appropriate for reporting a click.
1195 */
1196 private Uri getClickReportingUri() {
1197 Uri.Builder uriBuilder = new Uri.Builder()
1198 .scheme(ContentResolver.SCHEME_CONTENT)
1199 .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY);
1200
1201 uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH);
1202
1203 return uriBuilder
1204 .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1205 .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1206 .build();
1207 }
1208
1209 /**
1210 * Wraps an icon for a particular package. If the icon is a resource id, it is converted into
1211 * an android.resource:// URI.
1212 *
1213 * @param source The source of the icon
1214 * @param icon The icon retrieved from a suggestion column
1215 * @return An icon string appropriate for the package.
1216 */
1217 private String wrapIconForPackage(ComponentName source, String icon) {
1218 if (icon == null || icon.length() == 0 || "0".equals(icon)) {
1219 // SearchManager specifies that null or zero can be returned to indicate
1220 // no icon. We also allow empty string.
1221 return null;
1222 } else if (!Character.isDigit(icon.charAt(0))){
1223 return icon;
1224 } else {
1225 String packageName = source.getPackageName();
1226 return new Uri.Builder()
1227 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
1228 .authority(packageName)
1229 .encodedPath(icon)
1230 .toString();
1231 }
1232 }
1233
1234 /**
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001235 * Launches an intent and dismisses the search dialog (unless the intent
1236 * is one of the special intents that modifies the state of the search dialog).
Karl Rosaen875d50a2009-04-23 19:00:21 -07001237 */
1238 private void launchIntent(Intent intent) {
1239 if (intent == null) {
1240 return;
1241 }
1242 if (handleSpecialIntent(intent)){
1243 return;
1244 }
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001245 dismiss();
Karl Rosaen875d50a2009-04-23 19:00:21 -07001246 getContext().startActivity(intent);
1247 }
1248
1249 /**
1250 * Handles the special intent actions declared in {@link SearchManager}.
1251 *
1252 * @return <code>true</code> if the intent was handled.
1253 */
1254 private boolean handleSpecialIntent(Intent intent) {
1255 String action = intent.getAction();
1256 if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
1257 handleChangeSourceIntent(intent);
1258 return true;
Karl Rosaen875d50a2009-04-23 19:00:21 -07001259 }
1260 return false;
1261 }
1262
1263 /**
Karl Rosaend4c98c42009-06-09 17:05:54 +01001264 * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001265 */
1266 private void handleChangeSourceIntent(Intent intent) {
1267 Uri dataUri = intent.getData();
1268 if (dataUri == null) {
1269 Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
1270 return;
1271 }
1272 ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
1273 if (componentName == null) {
1274 Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
1275 return;
1276 }
1277 if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);
1278
1279 ComponentName previous = mLaunchComponent;
1280 if (!show(componentName, mAppSearchData, false)) {
1281 Log.w(LOG_TAG, "Failed to switch to source " + componentName);
1282 return;
1283 }
1284 pushPreviousComponent(previous);
1285
1286 String query = intent.getStringExtra(SearchManager.QUERY);
1287 setUserQuery(query);
Mike LeBeau35df87c2009-06-24 13:06:39 -07001288 mSearchAutoComplete.showDropDown();
Karl Rosaen875d50a2009-04-23 19:00:21 -07001289 }
Karl Rosaena058f022009-06-01 23:11:44 +01001290
Karl Rosaen875d50a2009-04-23 19:00:21 -07001291 /**
Mike LeBeauae9760b2009-06-01 21:53:09 +01001292 * Sets the list item selection in the AutoCompleteTextView's ListView.
1293 */
1294 public void setListSelection(int index) {
1295 mSearchAutoComplete.setListSelection(index);
1296 }
Karl Rosaena058f022009-06-01 23:11:44 +01001297
Mike LeBeauae9760b2009-06-01 21:53:09 +01001298 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001299 * Saves the previous component that was searched, so that we can go
1300 * back to it.
1301 */
1302 private void pushPreviousComponent(ComponentName componentName) {
1303 if (mPreviousComponents == null) {
1304 mPreviousComponents = new ArrayList<ComponentName>();
1305 }
1306 mPreviousComponents.add(componentName);
1307 }
1308
1309 /**
1310 * Pops the previous component off the stack and returns it.
1311 *
1312 * @return The component name, or <code>null</code> if there was
1313 * no previous component.
1314 */
1315 private ComponentName popPreviousComponent() {
1316 if (mPreviousComponents == null) {
1317 return null;
1318 }
1319 int size = mPreviousComponents.size();
1320 if (size == 0) {
1321 return null;
1322 }
1323 return mPreviousComponents.remove(size - 1);
1324 }
1325
1326 /**
1327 * Goes back to the previous component that was searched, if any.
1328 *
1329 * @return <code>true</code> if there was a previous component that we could go back to.
1330 */
1331 private boolean backToPreviousComponent() {
1332 ComponentName previous = popPreviousComponent();
1333 if (previous == null) {
1334 return false;
1335 }
1336 if (!show(previous, mAppSearchData, false)) {
1337 Log.w(LOG_TAG, "Failed to switch to source " + previous);
1338 return false;
1339 }
1340
1341 // must touch text to trigger suggestions
1342 // TODO: should this be the text as it was when the user left
1343 // the source that we are now going back to?
1344 String query = mSearchAutoComplete.getText().toString();
1345 setUserQuery(query);
1346
1347 return true;
1348 }
1349
1350 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001351 * When a particular suggestion has been selected, perform the various lookups required
1352 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1353 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1354 * the suggestion includes a data id.
1355 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001356 * @param c The suggestions cursor, moved to the row of the user's selection
Karl Rosaen875d50a2009-04-23 19:00:21 -07001357 * @param actionKey The key code of the action key that was pressed,
1358 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1359 * @param actionMsg The message for the action key that was pressed,
1360 * or <code>null</code> if none.
1361 * @return An intent for the suggestion at the cursor's position.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001362 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001363 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001364 try {
1365 // use specific action if supplied, or default action if supplied, or fixed default
Karl Rosaen875d50a2009-04-23 19:00:21 -07001366 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
Karl Rosaena058f022009-06-01 23:11:44 +01001367
1368 // some items are display only, or have effect via the cursor respond click reporting.
1369 if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
1370 return null;
1371 }
1372
Karl Rosaen875d50a2009-04-23 19:00:21 -07001373 if (action == null) {
1374 action = mSearchable.getSuggestIntentAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001375 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001376 if (action == null) {
1377 action = Intent.ACTION_SEARCH;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001378 }
1379
1380 // use specific data if supplied, or default data if supplied
Karl Rosaen875d50a2009-04-23 19:00:21 -07001381 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001382 if (data == null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001383 data = mSearchable.getSuggestIntentData();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001384 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001385 // then, if an ID was provided, append it.
1386 if (data != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001387 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1388 if (id != null) {
1389 data = data + "/" + Uri.encode(id);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001390 }
1391 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001392 Uri dataUri = (data == null) ? null : Uri.parse(data);
1393
Satish Sampathbf23fe02009-06-15 23:47:56 +01001394 String componentName = getColumnString(
1395 c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
Karl Rosaena058f022009-06-01 23:11:44 +01001396
Karl Rosaen875d50a2009-04-23 19:00:21 -07001397 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
Satish Sampathbf23fe02009-06-15 23:47:56 +01001398 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001399
Satish Sampathbf23fe02009-06-15 23:47:56 +01001400 return createIntent(action, dataUri, extraData, query, componentName, actionKey,
1401 actionMsg);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001402 } catch (RuntimeException e ) {
1403 int rowNum;
1404 try { // be really paranoid now
1405 rowNum = c.getPosition();
1406 } catch (RuntimeException e2 ) {
1407 rowNum = -1;
1408 }
1409 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1410 " returned exception" + e.toString());
Karl Rosaen875d50a2009-04-23 19:00:21 -07001411 return null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001412 }
1413 }
1414
1415 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001416 * Constructs an intent from the given information and the search dialog state.
1417 *
1418 * @param action Intent action.
1419 * @param data Intent data, or <code>null</code>.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001420 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
Satish Sampathbf23fe02009-06-15 23:47:56 +01001421 * @param query Intent query, or <code>null</code>.
1422 * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001423 * @param actionKey The key code of the action key that was pressed,
1424 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1425 * @param actionMsg The message for the action key that was pressed,
1426 * or <code>null</code> if none.
1427 * @return The intent.
1428 */
Satish Sampathbf23fe02009-06-15 23:47:56 +01001429 private Intent createIntent(String action, Uri data, String extraData, String query,
1430 String componentName, int actionKey, String actionMsg) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001431 // Now build the Intent
1432 Intent intent = new Intent(action);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001433 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001434 if (data != null) {
1435 intent.setData(data);
1436 }
Bjorn Bringert5f806052009-06-24 12:02:26 +01001437 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001438 if (query != null) {
1439 intent.putExtra(SearchManager.QUERY, query);
1440 }
1441 if (extraData != null) {
1442 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1443 }
Satish Sampathbf23fe02009-06-15 23:47:56 +01001444 if (componentName != null) {
1445 intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName);
1446 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001447 if (mAppSearchData != null) {
1448 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1449 }
1450 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1451 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1452 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1453 }
1454 // attempt to enforce security requirement (no 3rd-party intents)
Bjorn Bringerta9204132009-05-05 14:06:35 +01001455 intent.setComponent(mSearchable.getSearchActivity());
Karl Rosaen875d50a2009-04-23 19:00:21 -07001456 return intent;
1457 }
1458
1459 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001460 * For a given suggestion and a given cursor row, get the action message. If not provided
1461 * by the specific row/column, also check for a single definition (for the action key).
1462 *
1463 * @param c The cursor providing suggestions
1464 * @param actionKey The actionkey record being examined
1465 *
1466 * @return Returns a string, or null if no action key message for this suggestion
1467 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001468 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001469 String result = null;
1470 // check first in the cursor data, for a suggestion-specific message
Bjorn Bringerta9204132009-05-05 14:06:35 +01001471 final String column = actionKey.getSuggestActionMsgColumn();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001472 if (column != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001473 result = SuggestionsAdapter.getColumnString(c, column);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001474 }
1475 // If the cursor didn't give us a message, see if there's a single message defined
1476 // for the actionkey (for all suggestions)
1477 if (result == null) {
Bjorn Bringerta9204132009-05-05 14:06:35 +01001478 result = actionKey.getSuggestActionMsg();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001479 }
1480 return result;
1481 }
1482
1483 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001484 * Local subclass for AutoCompleteTextView.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001485 */
1486 public static class SearchAutoComplete extends AutoCompleteTextView {
1487
Karl Rosaen875d50a2009-04-23 19:00:21 -07001488 private int mThreshold;
1489 private SearchDialog mSearchDialog;
1490
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001491 public SearchAutoComplete(Context context) {
Marco Nelissen1746d6f2009-05-14 13:29:24 -07001492 super(context);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001493 mThreshold = getThreshold();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001494 }
1495
1496 public SearchAutoComplete(Context context, AttributeSet attrs) {
1497 super(context, attrs);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001498 mThreshold = getThreshold();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001499 }
1500
1501 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1502 super(context, attrs, defStyle);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001503 mThreshold = getThreshold();
1504 }
1505
1506 private void setSearchDialog(SearchDialog searchDialog) {
1507 mSearchDialog = searchDialog;
1508 }
1509
1510 @Override
1511 public void setThreshold(int threshold) {
1512 super.setThreshold(threshold);
1513 mThreshold = threshold;
1514 }
1515
1516 /**
1517 * Returns true if the text field is empty, or contains only whitespace.
1518 */
1519 private boolean isEmpty() {
1520 return TextUtils.getTrimmedLength(getText()) == 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001521 }
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001522
Karl Rosaen875d50a2009-04-23 19:00:21 -07001523 /**
1524 * We override this method to avoid replacing the query box text
1525 * when a suggestion is clicked.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001526 */
1527 @Override
Karl Rosaen875d50a2009-04-23 19:00:21 -07001528 protected void replaceText(CharSequence text) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001529 }
1530
1531 /**
Mike LeBeau617202a2009-07-06 14:29:25 -07001532 * We override this method to avoid an extra onItemClick being called on the
1533 * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)}
1534 * when an item is clicked with the trackball.
1535 */
1536 @Override
1537 public void performCompletion() {
1538 }
Mike LeBeau1fffbd92009-07-14 15:57:41 -07001539
1540 /**
1541 * We override this method to be sure and show the soft keyboard if appropriate when
1542 * the TextView has focus.
1543 */
1544 @Override
1545 public void onWindowFocusChanged(boolean hasWindowFocus) {
1546 super.onWindowFocusChanged(hasWindowFocus);
1547
1548 if (hasWindowFocus) {
1549 InputMethodManager inputManager = (InputMethodManager)
1550 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1551 inputManager.showSoftInput(this, 0);
1552 }
1553 }
1554
Mike LeBeau617202a2009-07-06 14:29:25 -07001555 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001556 * 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 -08001557 */
1558 @Override
1559 public boolean enoughToFilter() {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001560 return mThreshold <= 0 || super.enoughToFilter();
1561 }
Karl Rosaen98e333f2009-04-28 10:39:09 -07001562
Karl Rosaen875d50a2009-04-23 19:00:21 -07001563 /**
1564 * {@link AutoCompleteTextView#onKeyPreIme(int, KeyEvent)}) dismisses the drop-down on BACK,
1565 * so we must override this method to modify the BACK behavior.
1566 */
1567 @Override
1568 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
Bjorn Bringertee716fa2009-07-16 09:15:37 +01001569 if (mSearchDialog.mSearchable == null) {
1570 return false;
1571 }
Karl Rosaen98e333f2009-04-28 10:39:09 -07001572 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
1573 if (mSearchDialog.backToPreviousComponent()) {
1574 return true;
1575 }
1576 return false; // will dismiss soft keyboard if necessary
1577 }
1578 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001579 }
1580 }
1581
Karl Rosaen875d50a2009-04-23 19:00:21 -07001582 protected boolean handleBackKey(int keyCode, KeyEvent event) {
1583 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001584 if (backToPreviousComponent()) {
1585 return true;
1586 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001587 cancel();
1588 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001589 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001590 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001591 }
1592
1593 /**
1594 * Implements OnItemClickListener
1595 */
1596 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001597 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1598 launchSuggestion(position);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001599 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001600
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001601 /**
1602 * Implements OnItemSelectedListener
1603 */
1604 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001605 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1606 // A suggestion has been selected, rewrite the query if possible,
1607 // otherwise the restore the original query.
1608 if (REWRITE_QUERIES) {
1609 rewriteQueryFromSuggestion(position);
1610 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001611 }
1612
1613 /**
1614 * Implements OnItemSelectedListener
1615 */
1616 public void onNothingSelected(AdapterView<?> parent) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001617 if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
1618 }
1619
1620 /**
1621 * Query rewriting.
1622 */
1623
1624 private void rewriteQueryFromSuggestion(int position) {
1625 Cursor c = mSuggestionsAdapter.getCursor();
1626 if (c == null) {
1627 return;
1628 }
1629 if (c.moveToPosition(position)) {
1630 // Get the new query from the suggestion.
1631 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1632 if (newQuery != null) {
1633 // The suggestion rewrites the query.
1634 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
1635 // Update the text field, without getting new suggestions.
1636 setQuery(newQuery);
1637 } else {
1638 // The suggestion does not rewrite the query, restore the user's query.
1639 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
1640 restoreUserQuery();
1641 }
1642 } else {
1643 // We got a bad position, restore the user's query.
1644 Log.w(LOG_TAG, "Bad suggestion position: " + position);
1645 restoreUserQuery();
1646 }
1647 }
1648
1649 /**
1650 * Restores the query entered by the user if needed.
1651 */
1652 private void restoreUserQuery() {
1653 if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
1654 setQuery(mUserQuery);
1655 }
1656
1657 /**
1658 * Sets the text in the query box, without updating the suggestions.
1659 */
1660 private void setQuery(CharSequence query) {
1661 mSearchAutoComplete.setText(query, false);
1662 if (query != null) {
1663 mSearchAutoComplete.setSelection(query.length());
1664 }
1665 }
1666
1667 /**
1668 * Sets the text in the query box, updating the suggestions.
1669 */
1670 private void setUserQuery(String query) {
1671 if (query == null) {
1672 query = "";
1673 }
1674 mUserQuery = query;
1675 mSearchAutoComplete.setText(query);
1676 mSearchAutoComplete.setSelection(query.length());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001677 }
1678
1679 /**
1680 * Debugging Support
1681 */
1682
1683 /**
1684 * For debugging only, sample the millisecond clock and log it.
1685 * Uses AtomicLong so we can use in multiple threads
1686 */
1687 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
1688 private void dbgLogTiming(final String caller) {
1689 long millis = SystemClock.uptimeMillis();
1690 long oldTime = mLastLogTime.getAndSet(millis);
1691 long delta = millis - oldTime;
1692 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
1693 Log.d(LOG_TAG,report);
1694 }
1695}