blob: e70b57091054f9b2eff95a9418ec0a8f7b552be8 [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.Configuration;
32import android.content.res.Resources;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033import android.database.Cursor;
Romain Guyb5537c42009-06-30 12:39:18 -070034import android.graphics.drawable.Animatable;
Bjorn Bringert444c7272009-07-06 21:32:50 +010035import android.graphics.drawable.Drawable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036import android.net.Uri;
37import android.os.Bundle;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038import android.os.SystemClock;
39import android.server.search.SearchableInfo;
40import android.speech.RecognizerIntent;
41import android.text.Editable;
42import android.text.InputType;
43import android.text.TextUtils;
44import android.text.TextWatcher;
Satish Sampath662df0b2009-06-22 23:16:07 +010045import android.text.util.Regex;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046import android.util.AttributeSet;
47import android.util.Log;
Mike LeBeaua97f4a12009-05-23 01:19:36 -050048import android.view.ContextThemeWrapper;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080049import android.view.Gravity;
50import android.view.KeyEvent;
Karl Rosaen875d50a2009-04-23 19:00:21 -070051import android.view.MotionEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052import android.view.View;
Karl Rosaen875d50a2009-04-23 19:00:21 -070053import android.view.ViewConfiguration;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054import android.view.ViewGroup;
55import android.view.Window;
56import android.view.WindowManager;
Satish Sampath662df0b2009-06-22 23:16:07 +010057import android.view.inputmethod.EditorInfo;
Dianne Hackborna8f556e2009-03-24 20:47:50 -070058import android.view.inputmethod.InputMethodManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080059import android.widget.AdapterView;
60import android.widget.AutoCompleteTextView;
61import android.widget.Button;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062import android.widget.ImageButton;
Mike LeBeau1fd73232009-04-27 19:12:05 -070063import android.widget.ImageView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064import android.widget.ListView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080065import android.widget.TextView;
Bjorn Bringertc1f40962009-04-29 13:08:39 +010066import android.widget.AdapterView.OnItemClickListener;
67import android.widget.AdapterView.OnItemSelectedListener;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068
Karl Rosaen875d50a2009-04-23 19:00:21 -070069import java.util.ArrayList;
70import java.util.WeakHashMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071import java.util.concurrent.atomic.AtomicLong;
72
73/**
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +010074 * System search dialog. This is controlled by the
75 * SearchManagerService and runs in the system process.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080076 *
77 * @hide
78 */
79public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
80
81 // Debugging support
Karl Rosaen875d50a2009-04-23 19:00:21 -070082 private static final boolean DBG = false;
83 private static final String LOG_TAG = "SearchDialog";
84 private static final boolean DBG_LOG_TIMING = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080085
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080086 private static final String INSTANCE_KEY_COMPONENT = "comp";
87 private static final String INSTANCE_KEY_APPDATA = "data";
88 private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +010089 private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp";
90 private static final String INSTANCE_KEY_STORED_APPDATA = "sData";
91 private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev";
92 private static final String INSTANCE_KEY_USER_QUERY = "uQry";
Mike LeBeau260dfb52009-07-15 15:20:14 -070093
94 // The extra key used in an intent to the speech recognizer for in-app voice search.
95 private static final String EXTRA_CALLING_PACKAGE = "calling_package";
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +010096
Mike LeBeau1fd73232009-04-27 19:12:05 -070097 private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12;
98 private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7;
Bjorn Bringert444c7272009-07-06 21:32:50 +010099
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800100 // views & widgets
101 private TextView mBadgeLabel;
Mike LeBeau1fd73232009-04-27 19:12:05 -0700102 private ImageView mAppIcon;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700103 private SearchAutoComplete mSearchAutoComplete;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800104 private Button mGoButton;
105 private ImageButton mVoiceButton;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700106 private View mSearchPlate;
Romain Guyf4f70462009-06-26 16:55:54 -0700107 private Drawable mWorkingSpinner;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108
109 // interaction with searchable application
Karl Rosaen875d50a2009-04-23 19:00:21 -0700110 private SearchableInfo mSearchable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111 private ComponentName mLaunchComponent;
112 private Bundle mAppSearchData;
113 private boolean mGlobalSearchMode;
114 private Context mActivityContext;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700115
Mike LeBeaub3aab692009-04-30 02:09:09 -0700116 // Values we store to allow user to toggle between in-app search and global search.
117 private ComponentName mStoredComponentName;
118 private Bundle mStoredAppSearchData;
119
Karl Rosaen875d50a2009-04-23 19:00:21 -0700120 // stack of previous searchables, to support the BACK key after
121 // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.
122 // The top of the stack (= previous searchable) is the last element of the list,
123 // since adding and removing is efficient at the end of an ArrayList.
124 private ArrayList<ComponentName> mPreviousComponents;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800125
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800126 // For voice searching
127 private Intent mVoiceWebSearchIntent;
128 private Intent mVoiceAppSearchIntent;
129
130 // support for AutoCompleteTextView suggestions display
131 private SuggestionsAdapter mSuggestionsAdapter;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700132
133 // Whether to rewrite queries when selecting suggestions
Mike LeBeaucce7dbc2009-06-18 09:25:18 -0700134 private static final boolean REWRITE_QUERIES = true;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700135
136 // The query entered by the user. This is not changed when selecting a suggestion
137 // that modifies the contents of the text field. But if the user then edits
138 // the suggestion, the resulting string is saved.
139 private String mUserQuery;
140
141 // A weak map of drawables we've gotten from other packages, so we don't load them
142 // more than once.
143 private final WeakHashMap<String, Drawable> mOutsideDrawablesCache =
144 new WeakHashMap<String, Drawable>();
Satish Sampath662df0b2009-06-22 23:16:07 +0100145
146 // Last known IME options value for the search edit text.
147 private int mSearchAutoCompleteImeOptions;
148
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800149 /**
150 * Constructor - fires it up and makes it look like the search UI.
151 *
152 * @param context Application Context we can use for system acess
153 */
154 public SearchDialog(Context context) {
Mike LeBeaua97f4a12009-05-23 01:19:36 -0500155 super(context, com.android.internal.R.style.Theme_GlobalSearchBar);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800156 }
157
158 /**
159 * We create the search dialog just once, and it stays around (hidden)
160 * until activated by the user.
161 */
162 @Override
163 protected void onCreate(Bundle savedInstanceState) {
164 super.onCreate(savedInstanceState);
165
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800166 setContentView(com.android.internal.R.layout.search_bar);
167
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100168 Window theWindow = getWindow();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800169 WindowManager.LayoutParams lp = theWindow.getAttributes();
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100170 lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR;
171 lp.width = ViewGroup.LayoutParams.FILL_PARENT;
172 // taking up the whole window (even when transparent) is less than ideal,
173 // but necessary to show the popup window until the window manager supports
174 // having windows anchored by their parent but not clipped by them.
175 lp.height = ViewGroup.LayoutParams.FILL_PARENT;
176 lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL;
Mike LeBeau98acd542009-05-07 19:04:39 -0700177 lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178 theWindow.setAttributes(lp);
179
180 // get the view elements for local access
181 mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700182 mSearchAutoComplete = (SearchAutoComplete)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800183 findViewById(com.android.internal.R.id.search_src_text);
Mike LeBeau1fd73232009-04-27 19:12:05 -0700184 mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800185 mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
186 mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700187 mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
Romain Guyf4f70462009-06-26 16:55:54 -0700188 mWorkingSpinner = getContext().getResources().
Mike LeBeau1480eb22009-05-20 17:22:13 -0700189 getDrawable(com.android.internal.R.drawable.search_spinner);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800190
191 // attach listeners
Karl Rosaen875d50a2009-04-23 19:00:21 -0700192 mSearchAutoComplete.addTextChangedListener(mTextWatcher);
193 mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
194 mSearchAutoComplete.setOnItemClickListener(this);
195 mSearchAutoComplete.setOnItemSelectedListener(this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800196 mGoButton.setOnClickListener(mGoButtonClickListener);
197 mGoButton.setOnKeyListener(mButtonsKeyListener);
198 mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
199 mVoiceButton.setOnKeyListener(mButtonsKeyListener);
200
Karl Rosaen875d50a2009-04-23 19:00:21 -0700201 mSearchAutoComplete.setSearchDialog(this);
202
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 // pre-hide all the extraneous elements
204 mBadgeLabel.setVisibility(View.GONE);
205
206 // Additional adjustments to make Dialog work for Search
207
208 // Touching outside of the search dialog will dismiss it
209 setCanceledOnTouchOutside(true);
Bjorn Bringert444c7272009-07-06 21:32:50 +0100210
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800211 // Save voice intent for later queries/launching
212 mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100213 mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800214 mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
215 RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
216
217 mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100218 mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Satish Sampath662df0b2009-06-22 23:16:07 +0100219
220 mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800221 }
222
223 /**
224 * Set up the search dialog
225 *
Karl Rosaen875d50a2009-04-23 19:00:21 -0700226 * @return true if search dialog launched, false if not
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800227 */
228 public boolean show(String initialQuery, boolean selectInitialQuery,
229 ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100230
Mike LeBeaub3aab692009-04-30 02:09:09 -0700231 // Reset any stored values from last time dialog was shown.
232 mStoredComponentName = null;
233 mStoredAppSearchData = null;
Satish Sampathfef8d3e2009-07-01 17:48:42 +0100234
235 boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData,
236 globalSearch);
237 if (success) {
238 // Display the drop down as soon as possible instead of waiting for the rest of the
239 // pending UI stuff to get done, so that things appear faster to the user.
240 mSearchAutoComplete.showDropDownAfterLayout();
241 }
242 return success;
Mike LeBeaub3aab692009-04-30 02:09:09 -0700243 }
244
Mike LeBeaub3aab692009-04-30 02:09:09 -0700245 /**
246 * Called in response to a press of the hard search button in
247 * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app
248 * search and global search when relevant.
249 *
250 * If pressed within an in-app search context, this switches the search dialog out to
251 * global search. If pressed within a global search context that was originally an in-app
252 * search context, this switches back to the in-app search context. If pressed within a
253 * global search context that has no original in-app search context (e.g., global search
254 * from Home), this does nothing.
255 *
256 * @return false if we wanted to toggle context but could not do so successfully, true
257 * in all other cases
258 */
259 private boolean toggleGlobalSearch() {
260 String currentSearchText = mSearchAutoComplete.getText().toString();
261 if (!mGlobalSearchMode) {
262 mStoredComponentName = mLaunchComponent;
263 mStoredAppSearchData = mAppSearchData;
264 return doShow(currentSearchText, false, null, mAppSearchData, true);
265 } else {
266 if (mStoredComponentName != null) {
267 // This means we should toggle *back* to an in-app search context from
268 // global search.
269 return doShow(currentSearchText, false, mStoredComponentName,
270 mStoredAppSearchData, false);
271 } else {
272 return true;
273 }
274 }
275 }
276
277 /**
278 * Does the rest of the work required to show the search dialog. Called by both
279 * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and
280 * {@link #toggleGlobalSearch()}.
281 *
282 * @return true if search dialog showed, false if not
283 */
284 private boolean doShow(String initialQuery, boolean selectInitialQuery,
285 ComponentName componentName, Bundle appSearchData,
286 boolean globalSearch) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700287 // set up the searchable and show the dialog
288 if (!show(componentName, appSearchData, globalSearch)) {
289 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800290 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700291
Karl Rosaen875d50a2009-04-23 19:00:21 -0700292 // finally, load the user's initial text (which may trigger suggestions)
293 setUserQuery(initialQuery);
294 if (selectInitialQuery) {
295 mSearchAutoComplete.selectAll();
296 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700297
Karl Rosaen875d50a2009-04-23 19:00:21 -0700298 return true;
299 }
Mike LeBeaub3aab692009-04-30 02:09:09 -0700300
Karl Rosaen875d50a2009-04-23 19:00:21 -0700301 /**
302 * Sets up the search dialog and shows it.
303 *
304 * @return <code>true</code> if search dialog launched
305 */
306 private boolean show(ComponentName componentName, Bundle appSearchData,
307 boolean globalSearch) {
308
309 if (DBG) {
310 Log.d(LOG_TAG, "show(" + componentName + ", "
311 + appSearchData + ", " + globalSearch + ")");
312 }
313
Bjorn Bringert8d153822009-06-22 10:31:44 +0100314 SearchManager searchManager = (SearchManager)
315 mContext.getSystemService(Context.SEARCH_SERVICE);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700316 // Try to get the searchable info for the provided component (or for global search,
317 // if globalSearch == true).
Bjorn Bringert8d153822009-06-22 10:31:44 +0100318 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700319
320 // If we got back nothing, and it wasn't a request for global search, then try again
321 // for global search, as we'll try to launch that in lieu of any component-specific search.
322 if (!globalSearch && mSearchable == null) {
323 globalSearch = true;
Bjorn Bringert8d153822009-06-22 10:31:44 +0100324 mSearchable = searchManager.getSearchableInfo(componentName, globalSearch);
Mike LeBeaua59d7b02009-05-12 15:30:37 -0700325
326 // If we still get back null (i.e., there's not even a searchable info available
327 // for global search), then really give up.
328 if (mSearchable == null) {
329 // Unfortunately, we can't log here. it would be logspam every time the user
330 // clicks the "search" key on a non-search app.
331 return false;
332 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800333 }
334
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800335 mLaunchComponent = componentName;
336 mAppSearchData = appSearchData;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700337 // Using globalSearch here is just an optimization, just calling
338 // isDefaultSearchable() should always give the same result.
Bjorn Bringert8d153822009-06-22 10:31:44 +0100339 mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700340 mActivityContext = mSearchable.getActivityContext(getContext());
341
342 // show the dialog. this will call onStart().
Mike LeBeau1fffbd92009-07-14 15:57:41 -0700343 if (!isShowing()) {
Mike LeBeaua97f4a12009-05-23 01:19:36 -0500344 // The Dialog uses a ContextThemeWrapper for the context; use this to change the
345 // theme out from underneath us, between the global search theme and the in-app
346 // search theme. They are identical except that the global search theme does not
347 // dim the background of the window (because global search is full screen so it's
348 // not needed and this should save a little bit of time on global search invocation).
349 Object context = getContext();
350 if (context instanceof ContextThemeWrapper) {
351 ContextThemeWrapper wrapper = (ContextThemeWrapper) context;
352 if (globalSearch) {
353 wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar);
354 } else {
355 wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar);
356 }
357 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700358 show();
359 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800360
Karl Rosaen875d50a2009-04-23 19:00:21 -0700361 updateUI();
362
363 return true;
364 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800365
366 /**
367 * The search dialog is being dismissed, so handle all of the local shutdown operations.
368 *
369 * This function is designed to be idempotent so that dismiss() can be safely called at any time
370 * (even if already closed) and more likely to really dump any memory. No leaks!
371 */
372 @Override
373 public void onStop() {
374 super.onStop();
Bjorn Bringert444c7272009-07-06 21:32:50 +0100375
Karl Rosaen875d50a2009-04-23 19:00:21 -0700376 closeSuggestionsAdapter();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377
378 // dump extra memory we're hanging on to
379 mLaunchComponent = null;
380 mAppSearchData = null;
381 mSearchable = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800382 mActivityContext = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800383 mUserQuery = null;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700384 mPreviousComponents = null;
385 }
Mike LeBeau1c690752009-05-20 20:20:26 -0700386
Mike LeBeau1c690752009-05-20 20:20:26 -0700387 /**
Mike LeBeau1480eb22009-05-20 17:22:13 -0700388 * Sets the search dialog to the 'working' state, which shows a working spinner in the
389 * right hand size of the text field.
390 *
391 * @param working true to show spinner, false to hide spinner
392 */
393 public void setWorking(boolean working) {
394 if (working) {
395 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
396 null, null, mWorkingSpinner, null);
Romain Guyb5537c42009-06-30 12:39:18 -0700397 ((Animatable) mWorkingSpinner).start();
Mike LeBeau1480eb22009-05-20 17:22:13 -0700398 } else {
399 mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds(
400 null, null, null, null);
Romain Guyb5537c42009-06-30 12:39:18 -0700401 ((Animatable) mWorkingSpinner).stop();
Mike LeBeau1480eb22009-05-20 17:22:13 -0700402 }
403 }
404
405 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700406 * Closes and gets rid of the suggestions adapter.
407 */
408 private void closeSuggestionsAdapter() {
409 // remove the adapter from the autocomplete first, to avoid any updates
410 // when we drop the cursor
411 mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
412 // close any leftover cursor
413 if (mSuggestionsAdapter != null) {
414 mSuggestionsAdapter.changeCursor(null);
415 }
416 mSuggestionsAdapter = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800417 }
418
419 /**
420 * Save the minimal set of data necessary to recreate the search
421 *
Bjorn Bringert444c7272009-07-06 21:32:50 +0100422 * @return A bundle with the state of the dialog, or {@code null} if the search
423 * dialog is not showing.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800424 */
425 @Override
426 public Bundle onSaveInstanceState() {
Bjorn Bringert444c7272009-07-06 21:32:50 +0100427 if (!isShowing()) return null;
428
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800429 Bundle bundle = new Bundle();
Bjorn Bringert444c7272009-07-06 21:32:50 +0100430
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800431 // setup info so I can recreate this particular search
432 bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent);
433 bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData);
434 bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +0100435 bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName);
436 bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData);
437 bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents);
438 bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
439
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800440 return bundle;
441 }
442
443 /**
444 * Restore the state of the dialog from a previously saved bundle.
Karl Rosaen875d50a2009-04-23 19:00:21 -0700445 *
446 * TODO: go through this and make sure that it saves everything that is saved
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800447 *
448 * @param savedInstanceState The state of the dialog previously saved by
449 * {@link #onSaveInstanceState()}.
450 */
451 @Override
452 public void onRestoreInstanceState(Bundle savedInstanceState) {
Bjorn Bringert444c7272009-07-06 21:32:50 +0100453 if (savedInstanceState == null) return;
454
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800455 ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT);
456 Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA);
457 boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH);
Bjorn Bringertb0ae27f2009-06-23 13:47:31 +0100458 ComponentName storedComponentName =
459 savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT);
460 Bundle storedAppSearchData =
461 savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA);
462 ArrayList<ComponentName> previousComponents =
463 savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS);
464 String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
465
466 // Set stored state
467 mStoredComponentName = storedComponentName;
468 mStoredAppSearchData = storedAppSearchData;
469 mPreviousComponents = previousComponents;
470
471 // show the dialog.
472 if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800473 // for some reason, we couldn't re-instantiate
474 return;
475 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800476 }
477
478 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700479 * Called after resources have changed, e.g. after screen rotation or locale change.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800480 */
Bjorn Bringert444c7272009-07-06 21:32:50 +0100481 public void onConfigurationChanged() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800482 if (isShowing()) {
483 // Redraw (resources may have changed)
484 updateSearchButton();
Mike LeBeau1fd73232009-04-27 19:12:05 -0700485 updateSearchAppIcon();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800486 updateSearchBadge();
487 updateQueryHint();
488 }
489 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700490
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800491 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700492 * Update the UI according to the info in the current value of {@link #mSearchable}.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800493 */
Karl Rosaen875d50a2009-04-23 19:00:21 -0700494 private void updateUI() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800495 if (mSearchable != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700496 updateSearchAutoComplete();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800497 updateSearchButton();
Mike LeBeau1fd73232009-04-27 19:12:05 -0700498 updateSearchAppIcon();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800499 updateSearchBadge();
500 updateQueryHint();
501 updateVoiceButton();
502
503 // In order to properly configure the input method (if one is being used), we
504 // need to let it know if we'll be providing suggestions. Although it would be
505 // difficult/expensive to know if every last detail has been configured properly, we
506 // can at least see if a suggestions provider has been configured, and use that
507 // as our trigger.
508 int inputType = mSearchable.getInputType();
509 // We only touch this if the input type is set up for text (which it almost certainly
510 // should be, in the case of search!)
511 if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
512 // The existence of a suggestions authority is the proxy for "suggestions
513 // are available here"
514 inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
515 if (mSearchable.getSuggestAuthority() != null) {
516 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
517 }
518 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700519 mSearchAutoComplete.setInputType(inputType);
Satish Sampath662df0b2009-06-22 23:16:07 +0100520 mSearchAutoCompleteImeOptions = mSearchable.getImeOptions();
521 mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700522 }
523 }
524
525 /**
526 * Updates the auto-complete text view.
527 */
528 private void updateSearchAutoComplete() {
529 // close any existing suggestions adapter
530 closeSuggestionsAdapter();
531
532 mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
Bjorn Bringert203464a2009-04-27 17:08:11 +0100533 mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +0100534 // we dismiss the entire dialog instead
535 mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700536
537 if (mGlobalSearchMode) {
538 mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in
Karl Rosaen875d50a2009-04-23 19:00:21 -0700539 } else {
540 mSearchAutoComplete.setDropDownAlwaysVisible(false);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700541 }
542
543 // attach the suggestions adapter, if suggestions are available
544 // The existence of a suggestions authority is the proxy for "suggestions available here"
545 if (mSearchable.getSuggestAuthority() != null) {
Mike LeBeau1480eb22009-05-20 17:22:13 -0700546 mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable,
547 mOutsideDrawablesCache, mGlobalSearchMode);
Karl Rosaen875d50a2009-04-23 19:00:21 -0700548 mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800549 }
550 }
551
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800552 /**
553 * Update the text in the search button. Note: This is deprecated functionality, for
554 * 1.0 compatibility only.
555 */
556 private void updateSearchButton() {
557 String textLabel = null;
558 Drawable iconLabel = null;
559 int textId = mSearchable.getSearchButtonText();
560 if (textId != 0) {
561 textLabel = mActivityContext.getResources().getString(textId);
562 } else {
563 iconLabel = getContext().getResources().
564 getDrawable(com.android.internal.R.drawable.ic_btn_search);
565 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700566 mGoButton.setText(textLabel);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800567 mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
568 }
569
Mike LeBeau1fd73232009-04-27 19:12:05 -0700570 private void updateSearchAppIcon() {
571 if (mGlobalSearchMode) {
572 mAppIcon.setImageResource(0);
573 mAppIcon.setVisibility(View.GONE);
574 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL,
575 mSearchPlate.getPaddingTop(),
576 mSearchPlate.getPaddingRight(),
577 mSearchPlate.getPaddingBottom());
578 } else {
579 PackageManager pm = getContext().getPackageManager();
Romain Guyf4f70462009-06-26 16:55:54 -0700580 Drawable icon;
Mike LeBeau1fd73232009-04-27 19:12:05 -0700581 try {
582 ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
583 icon = pm.getApplicationIcon(info.applicationInfo);
584 if (DBG) Log.d(LOG_TAG, "Using app-specific icon");
585 } catch (NameNotFoundException e) {
586 icon = pm.getDefaultActivityIcon();
587 Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon");
588 }
589 mAppIcon.setImageDrawable(icon);
590 mAppIcon.setVisibility(View.VISIBLE);
591 mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL,
592 mSearchPlate.getPaddingTop(),
593 mSearchPlate.getPaddingRight(),
594 mSearchPlate.getPaddingBottom());
595 }
596 }
597
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800598 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700599 * Setup the search "Badge" if requested by mode flags.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800600 */
601 private void updateSearchBadge() {
602 // assume both hidden
603 int visibility = View.GONE;
604 Drawable icon = null;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700605 CharSequence text = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800606
607 // optionally show one or the other.
Bjorn Bringerta9204132009-05-05 14:06:35 +0100608 if (mSearchable.useBadgeIcon()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800609 icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
610 visibility = View.VISIBLE;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700611 if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
Bjorn Bringerta9204132009-05-05 14:06:35 +0100612 } else if (mSearchable.useBadgeLabel()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800613 text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
614 visibility = View.VISIBLE;
Karl Rosaen875d50a2009-04-23 19:00:21 -0700615 if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800616 }
617
618 mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
619 mBadgeLabel.setText(text);
620 mBadgeLabel.setVisibility(visibility);
621 }
622
623 /**
624 * Update the hint in the query text field.
625 */
626 private void updateQueryHint() {
627 if (isShowing()) {
628 String hint = null;
629 if (mSearchable != null) {
630 int hintId = mSearchable.getHintId();
631 if (hintId != 0) {
632 hint = mActivityContext.getString(hintId);
633 }
634 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700635 mSearchAutoComplete.setHint(hint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800636 }
637 }
638
639 /**
640 * Update the visibility of the voice button. There are actually two voice search modes,
641 * either of which will activate the button.
642 */
643 private void updateVoiceButton() {
644 int visibility = View.GONE;
645 if (mSearchable.getVoiceSearchEnabled()) {
646 Intent testIntent = null;
647 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
648 testIntent = mVoiceWebSearchIntent;
649 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
650 testIntent = mVoiceAppSearchIntent;
651 }
652 if (testIntent != null) {
653 ResolveInfo ri = getContext().getPackageManager().
654 resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY);
655 if (ri != null) {
656 visibility = View.VISIBLE;
657 }
658 }
659 }
660 mVoiceButton.setVisibility(visibility);
661 }
662
663 /**
664 * Listeners of various types
665 */
666
667 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -0700668 * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
669 * touch is outside the window. But the window includes space for the drop-down,
670 * so we also cancel on taps outside the search bar when the drop-down is not showing.
671 */
672 @Override
673 public boolean onTouchEvent(MotionEvent event) {
674 // cancel if the drop-down is not showing and the touch event was outside the search plate
675 if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
676 if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
677 cancel();
678 return true;
679 }
680 // Let Dialog handle events outside the window while the pop-up is showing.
681 return super.onTouchEvent(event);
682 }
683
684 private boolean isOutOfBounds(View v, MotionEvent event) {
685 final int x = (int) event.getX();
686 final int y = (int) event.getY();
687 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
688 return (x < -slop) || (y < -slop)
689 || (x > (v.getWidth()+slop))
690 || (y > (v.getHeight()+slop));
691 }
692
693 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800694 * Dialog's OnKeyListener implements various search-specific functionality
695 *
696 * @param keyCode This is the keycode of the typed key, and is the same value as
Karl Rosaen875d50a2009-04-23 19:00:21 -0700697 * found in the KeyEvent parameter.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800698 * @param event The complete event record for the typed key
699 *
700 * @return Return true if the event was handled here, or false if not.
701 */
702 @Override
703 public boolean onKeyDown(int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700704 if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
705
706 // 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 }
741 updateWidgetState();
Karl Rosaen875d50a2009-04-23 19:00:21 -0700742 if (!mSearchAutoComplete.isPerformingCompletion()) {
743 // The user changed the query, remember it.
744 mUserQuery = s == null ? "" : s.toString();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800745 }
746 }
747
Satish Sampath662df0b2009-06-22 23:16:07 +0100748 public void afterTextChanged(Editable s) {
Dianne Hackbornb06ea702009-07-13 13:07:51 -0700749 if (mSearchable == null) {
750 return;
751 }
Satish Sampathd21572c2009-07-08 14:54:11 +0100752 if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) {
Satish Sampath662df0b2009-06-22 23:16:07 +0100753 // The user changed the query, check if it is a URL and if so change the search
754 // button in the soft keyboard to the 'Go' button.
755 int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION));
756 if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) {
757 options = options | EditorInfo.IME_ACTION_GO;
758 } else {
759 options = options | EditorInfo.IME_ACTION_SEARCH;
760 }
761 if (options != mSearchAutoCompleteImeOptions) {
762 mSearchAutoCompleteImeOptions = options;
763 mSearchAutoComplete.setImeOptions(options);
764 // This call is required to update the soft keyboard UI with latest IME flags.
765 mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType());
766 }
767 }
768 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800769 };
770
771 /**
772 * Enable/Disable the cancel button based on edit text state (any text?)
773 */
774 private void updateWidgetState() {
775 // enable the button if we have one or more non-space characters
Karl Rosaen875d50a2009-04-23 19:00:21 -0700776 boolean enabled = !mSearchAutoComplete.isEmpty();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800777 mGoButton.setEnabled(enabled);
778 mGoButton.setFocusable(enabled);
779 }
780
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800781 /**
782 * React to typing in the GO search button by refocusing to EditText.
783 * Continue typing the query.
784 */
785 View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
786 public boolean onKey(View v, int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700787 // guard against possible race conditions
788 if (mSearchable == null) {
789 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800790 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700791
792 if (!event.isSystem() &&
793 (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
794 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
795 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
796 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
797 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
798 // restore focus and give key to EditText ...
799 if (mSearchAutoComplete.requestFocus()) {
800 return mSearchAutoComplete.dispatchKeyEvent(event);
801 }
802 }
803
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800804 return false;
805 }
806 };
807
808 /**
809 * React to a click in the GO button by launching a search.
810 */
811 View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
812 public void onClick(View v) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700813 // guard against possible race conditions
814 if (mSearchable == null) {
815 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800816 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700817 launchQuerySearch();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800818 }
819 };
820
821 /**
822 * React to a click in the voice search button.
823 */
824 View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
825 public void onClick(View v) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700826 // guard against possible race conditions
827 if (mSearchable == null) {
828 return;
829 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800830 try {
831 if (mSearchable.getVoiceSearchLaunchWebSearch()) {
832 getContext().startActivity(mVoiceWebSearchIntent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800833 } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
834 Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
835 getContext().startActivity(appSearchIntent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800836 }
837 } catch (ActivityNotFoundException e) {
838 // Should not happen, since we check the availability of
839 // voice search before showing the button. But just in case...
840 Log.w(LOG_TAG, "Could not find voice search activity");
841 }
842 }
843 };
844
845 /**
846 * Create and return an Intent that can launch the voice search activity, perform a specific
847 * voice transcription, and forward the results to the searchable activity.
848 *
849 * @param baseIntent The voice app search intent to start from
850 * @return A completely-configured intent ready to send to the voice search activity
851 */
852 private Intent createVoiceAppSearchIntent(Intent baseIntent) {
Mike LeBeau260dfb52009-07-15 15:20:14 -0700853 ComponentName searchActivity = mSearchable.getSearchActivity();
854
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800855 // create the necessary intent to set up a search-and-forward operation
856 // in the voice search system. We have to keep the bundle separate,
857 // because it becomes immutable once it enters the PendingIntent
858 Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
Mike LeBeau260dfb52009-07-15 15:20:14 -0700859 queryIntent.setComponent(searchActivity);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800860 PendingIntent pending = PendingIntent.getActivity(
861 getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
862
863 // Now set up the bundle that will be inserted into the pending intent
864 // when it's time to do the search. We always build it here (even if empty)
865 // because the voice search activity will always need to insert "QUERY" into
866 // it anyway.
867 Bundle queryExtras = new Bundle();
868 if (mAppSearchData != null) {
869 queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData);
870 }
871
872 // Now build the intent to launch the voice search. Add all necessary
873 // extras to launch the voice recognizer, and then all the necessary extras
874 // to forward the results to the searchable activity
875 Intent voiceIntent = new Intent(baseIntent);
876
877 // Add all of the configuration options supplied by the searchable's metadata
878 String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
879 String prompt = null;
880 String language = null;
881 int maxResults = 1;
882 Resources resources = mActivityContext.getResources();
883 if (mSearchable.getVoiceLanguageModeId() != 0) {
884 languageModel = resources.getString(mSearchable.getVoiceLanguageModeId());
885 }
886 if (mSearchable.getVoicePromptTextId() != 0) {
887 prompt = resources.getString(mSearchable.getVoicePromptTextId());
888 }
889 if (mSearchable.getVoiceLanguageId() != 0) {
890 language = resources.getString(mSearchable.getVoiceLanguageId());
891 }
892 if (mSearchable.getVoiceMaxResults() != 0) {
893 maxResults = mSearchable.getVoiceMaxResults();
894 }
895 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
896 voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
897 voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
898 voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
Mike LeBeau260dfb52009-07-15 15:20:14 -0700899 voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
900 searchActivity == null ? null : searchActivity.toShortString());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800901
902 // Add the values that configure forwarding the results
903 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
904 voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
905
906 return voiceIntent;
907 }
908
909 /**
Satish Sampath662df0b2009-06-22 23:16:07 +0100910 * Corrects http/https typo errors in the given url string, and if the protocol specifier was
911 * not present defaults to http.
912 *
913 * @param inUrl URL to check and fix
914 * @return fixed URL string.
915 */
916 private String fixUrl(String inUrl) {
917 if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
918 return inUrl;
919
920 if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) {
921 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
922 inUrl = inUrl.replaceFirst("/", "//");
923 } else {
924 inUrl = inUrl.replaceFirst(":", "://");
925 }
926 }
927
928 if (inUrl.indexOf("://") == -1) {
929 inUrl = "http://" + inUrl;
930 }
931
932 return inUrl;
933 }
934
935 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800936 * React to the user typing "enter" or other hardwired keys while typing in the search box.
937 * This handles these special keys while the edit box has focus.
938 */
939 View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
940 public boolean onKey(View v, int keyCode, KeyEvent event) {
Karl Rosaen875d50a2009-04-23 19:00:21 -0700941 // guard against possible race conditions
942 if (mSearchable == null) {
943 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800944 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700945
946 if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
947 if (DBG) {
948 Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
949 + "), selection: " + mSearchAutoComplete.getListSelection());
950 }
951
952 // If a suggestion is selected, handle enter, search key, and action keys
953 // as presses on the selected suggestion
954 if (mSearchAutoComplete.isPopupShowing() &&
955 mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
956 return onSuggestionsKey(v, keyCode, event);
957 }
958
959 // If there is text in the query box, handle enter, and action keys
960 // The search key is handled by the dialog's onKeyDown().
961 if (!mSearchAutoComplete.isEmpty()) {
962 if (keyCode == KeyEvent.KEYCODE_ENTER
963 && event.getAction() == KeyEvent.ACTION_UP) {
964 v.cancelLongPress();
Satish Sampath662df0b2009-06-22 23:16:07 +0100965
Satish Sampathb1665f22009-07-10 15:43:38 +0100966 // If this is a url entered by the user & we displayed the 'Go' button which
967 // the user clicked, launch the url instead of using it as a search query.
968 if (mSearchable.autoUrlDetect() &&
969 (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION)
Satish Sampathd21572c2009-07-08 14:54:11 +0100970 == EditorInfo.IME_ACTION_GO) {
Satish Sampathb1665f22009-07-10 15:43:38 +0100971 Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString()));
972 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
973 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
974 launchIntent(intent);
975 } else {
976 // Launch as a regular search.
977 launchQuerySearch();
Satish Sampath662df0b2009-06-22 23:16:07 +0100978 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700979 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800980 }
Karl Rosaen875d50a2009-04-23 19:00:21 -0700981 if (event.getAction() == KeyEvent.ACTION_DOWN) {
982 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
Bjorn Bringerta9204132009-05-05 14:06:35 +0100983 if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
984 launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800985 return true;
986 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800987 }
988 }
989 return false;
990 }
991 };
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800992
Dianne Hackborna8f556e2009-03-24 20:47:50 -0700993 @Override
Bjorn Bringertec8ee342009-07-08 21:49:42 +0100994 public void dismiss() {
Bjorn Bringert444c7272009-07-06 21:32:50 +0100995 if (!isShowing()) return;
996
Dianne Hackborna8f556e2009-03-24 20:47:50 -0700997 // We made sure the IME was displayed, so also make sure it is closed
998 // when we go away.
999 InputMethodManager imm = (InputMethodManager)getContext()
1000 .getSystemService(Context.INPUT_METHOD_SERVICE);
1001 if (imm != null) {
1002 imm.hideSoftInputFromWindow(
1003 getWindow().getDecorView().getWindowToken(), 0);
1004 }
1005
Bjorn Bringertec8ee342009-07-08 21:49:42 +01001006 super.dismiss();
Dianne Hackborna8f556e2009-03-24 20:47:50 -07001007 }
1008
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001009 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001010 * React to the user typing while in the suggestions list. First, check for action
1011 * keys. If not handled, try refocusing regular characters into the EditText.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001012 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001013 private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
1014 // guard against possible race conditions (late arrival after dismiss)
1015 if (mSearchable == null) {
1016 return false;
1017 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001018 if (mSuggestionsAdapter == null) {
1019 return false;
1020 }
1021 if (event.getAction() == KeyEvent.ACTION_DOWN) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001022 if (DBG_LOG_TIMING) {
1023 dbgLogTiming("onSuggestionsKey()");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001024 }
1025
1026 // First, check for enter or search (both of which we'll treat as a "click")
1027 if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001028 int position = mSearchAutoComplete.getListSelection();
1029 return launchSuggestion(position);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001030 }
1031
1032 // Next, check for left/right moves, which we use to "return" the user to the edit view
1033 if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001034 // give "focus" to text editor, with cursor at the beginning if
1035 // left key, at end if right key
1036 // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001037 int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
Karl Rosaen875d50a2009-04-23 19:00:21 -07001038 0 : mSearchAutoComplete.length();
1039 mSearchAutoComplete.setSelection(selPoint);
1040 mSearchAutoComplete.setListSelection(0);
1041 mSearchAutoComplete.clearListSelection();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001042 return true;
1043 }
1044
1045 // Next, check for an "up and out" move
Karl Rosaen875d50a2009-04-23 19:00:21 -07001046 if (keyCode == KeyEvent.KEYCODE_DPAD_UP
1047 && 0 == mSearchAutoComplete.getListSelection()) {
1048 restoreUserQuery();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001049 // let ACTV complete the move
1050 return false;
1051 }
1052
1053 // Next, check for an "action key"
1054 SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
1055 if ((actionKey != null) &&
Bjorn Bringerta9204132009-05-05 14:06:35 +01001056 ((actionKey.getSuggestActionMsg() != null) ||
1057 (actionKey.getSuggestActionMsgColumn() != null))) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001058 // launch suggestion using action key column
1059 int position = mSearchAutoComplete.getListSelection();
1060 if (position != ListView.INVALID_POSITION) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001061 Cursor c = mSuggestionsAdapter.getCursor();
1062 if (c.moveToPosition(position)) {
1063 final String actionMsg = getActionKeyMessage(c, actionKey);
1064 if (actionMsg != null && (actionMsg.length() > 0)) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001065 return launchSuggestion(position, keyCode, actionMsg);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001066 }
1067 }
1068 }
1069 }
1070 }
1071 return false;
Karl Rosaen875d50a2009-04-23 19:00:21 -07001072 }
1073
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001074 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001075 * Launch a search for the text in the query text field.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001076 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001077 protected void launchQuerySearch() {
1078 launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001079 }
1080
1081 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001082 * Launch a search for the text in the query text field.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001083 *
Karl Rosaen875d50a2009-04-23 19:00:21 -07001084 * @param actionKey The key code of the action key that was pressed,
1085 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1086 * @param actionMsg The message for the action key that was pressed,
1087 * or <code>null</code> if none.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001088 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001089 protected void launchQuerySearch(int actionKey, String actionMsg) {
1090 String query = mSearchAutoComplete.getText().toString();
Satish Sampathbf23fe02009-06-15 23:47:56 +01001091 Intent intent = createIntent(Intent.ACTION_SEARCH, null, null, query, null,
Karl Rosaen875d50a2009-04-23 19:00:21 -07001092 actionKey, actionMsg);
1093 launchIntent(intent);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001094 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001095
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001096 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001097 * Launches an intent based on a suggestion.
1098 *
1099 * @param position The index of the suggestion to create the intent from.
1100 * @return true if a successful launch, false if could not (e.g. bad position).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001101 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001102 protected boolean launchSuggestion(int position) {
1103 return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
1104 }
1105
1106 /**
1107 * Launches an intent based on a suggestion.
1108 *
1109 * @param position The index of the suggestion to create the intent from.
1110 * @param actionKey The key code of the action key that was pressed,
1111 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1112 * @param actionMsg The message for the action key that was pressed,
1113 * or <code>null</code> if none.
1114 * @return true if a successful launch, false if could not (e.g. bad position).
1115 */
1116 protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
1117 Cursor c = mSuggestionsAdapter.getCursor();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001118 if ((c != null) && c.moveToPosition(position)) {
Karl Rosaend4c98c42009-06-09 17:05:54 +01001119
1120 Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
1121
1122 // report back about the click
1123 if (mGlobalSearchMode) {
1124 // in global search mode, do it via cursor
1125 mSuggestionsAdapter.callCursorOnClick(c, position);
1126 } else if (intent != null
1127 && mPreviousComponents != null
1128 && !mPreviousComponents.isEmpty()) {
1129 // in-app search (and we have pivoted in as told by mPreviousComponents,
1130 // which is used for keeping track of what we pop back to when we are pivoting into
1131 // in app search.)
1132 reportInAppClickToGlobalSearch(c, intent);
1133 }
Karl Rosaena058f022009-06-01 23:11:44 +01001134
1135 // launch the intent
Karl Rosaen875d50a2009-04-23 19:00:21 -07001136 launchIntent(intent);
Karl Rosaena058f022009-06-01 23:11:44 +01001137
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001138 return true;
1139 }
1140 return false;
1141 }
Karl Rosaena058f022009-06-01 23:11:44 +01001142
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001143 /**
Karl Rosaend4c98c42009-06-09 17:05:54 +01001144 * Report a click from an in app search result back to global search for shortcutting porpoises.
1145 *
1146 * @param c The cursor that is pointing to the clicked position.
1147 * @param intent The intent that will be launched for the click.
1148 */
1149 private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) {
1150 // for in app search, still tell global search via content provider
1151 Uri uri = getClickReportingUri();
1152 final ContentValues cv = new ContentValues();
1153 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery);
1154 final ComponentName source = mSearchable.getSearchActivity();
1155 cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString());
1156
1157 // grab the intent columns from the intent we created since it has additional
1158 // logic for falling back on the searchable default
1159 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction());
1160 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString());
Satish Sampathbf23fe02009-06-15 23:47:56 +01001161 cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME,
1162 intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY));
Karl Rosaend4c98c42009-06-09 17:05:54 +01001163
1164 // ensure the icons will work for global search
1165 cv.put(SearchManager.SUGGEST_COLUMN_ICON_1,
1166 wrapIconForPackage(
1167 source,
1168 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1)));
1169 cv.put(SearchManager.SUGGEST_COLUMN_ICON_2,
1170 wrapIconForPackage(
1171 source,
1172 getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2)));
1173
1174 // the rest can be passed through directly
1175 cv.put(SearchManager.SUGGEST_COLUMN_FORMAT,
1176 getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT));
1177 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
1178 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1));
1179 cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
1180 getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2));
1181 cv.put(SearchManager.SUGGEST_COLUMN_QUERY,
1182 getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY));
1183 cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
1184 getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID));
1185 // note: deliberately omitting background color since it is only for global search
1186 // "more results" entries
1187 mContext.getContentResolver().insert(uri, cv);
1188 }
1189
1190 /**
1191 * @return A URI appropriate for reporting a click.
1192 */
1193 private Uri getClickReportingUri() {
1194 Uri.Builder uriBuilder = new Uri.Builder()
1195 .scheme(ContentResolver.SCHEME_CONTENT)
1196 .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY);
1197
1198 uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH);
1199
1200 return uriBuilder
1201 .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1202 .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
1203 .build();
1204 }
1205
1206 /**
1207 * Wraps an icon for a particular package. If the icon is a resource id, it is converted into
1208 * an android.resource:// URI.
1209 *
1210 * @param source The source of the icon
1211 * @param icon The icon retrieved from a suggestion column
1212 * @return An icon string appropriate for the package.
1213 */
1214 private String wrapIconForPackage(ComponentName source, String icon) {
1215 if (icon == null || icon.length() == 0 || "0".equals(icon)) {
1216 // SearchManager specifies that null or zero can be returned to indicate
1217 // no icon. We also allow empty string.
1218 return null;
1219 } else if (!Character.isDigit(icon.charAt(0))){
1220 return icon;
1221 } else {
1222 String packageName = source.getPackageName();
1223 return new Uri.Builder()
1224 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
1225 .authority(packageName)
1226 .encodedPath(icon)
1227 .toString();
1228 }
1229 }
1230
1231 /**
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001232 * Launches an intent and dismisses the search dialog (unless the intent
1233 * is one of the special intents that modifies the state of the search dialog).
Karl Rosaen875d50a2009-04-23 19:00:21 -07001234 */
1235 private void launchIntent(Intent intent) {
1236 if (intent == null) {
1237 return;
1238 }
1239 if (handleSpecialIntent(intent)){
1240 return;
1241 }
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001242 dismiss();
Karl Rosaen875d50a2009-04-23 19:00:21 -07001243 getContext().startActivity(intent);
1244 }
1245
1246 /**
1247 * Handles the special intent actions declared in {@link SearchManager}.
1248 *
1249 * @return <code>true</code> if the intent was handled.
1250 */
1251 private boolean handleSpecialIntent(Intent intent) {
1252 String action = intent.getAction();
1253 if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
1254 handleChangeSourceIntent(intent);
1255 return true;
Karl Rosaen875d50a2009-04-23 19:00:21 -07001256 }
1257 return false;
1258 }
1259
1260 /**
Karl Rosaend4c98c42009-06-09 17:05:54 +01001261 * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001262 */
1263 private void handleChangeSourceIntent(Intent intent) {
1264 Uri dataUri = intent.getData();
1265 if (dataUri == null) {
1266 Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
1267 return;
1268 }
1269 ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
1270 if (componentName == null) {
1271 Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
1272 return;
1273 }
1274 if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);
1275
1276 ComponentName previous = mLaunchComponent;
1277 if (!show(componentName, mAppSearchData, false)) {
1278 Log.w(LOG_TAG, "Failed to switch to source " + componentName);
1279 return;
1280 }
1281 pushPreviousComponent(previous);
1282
1283 String query = intent.getStringExtra(SearchManager.QUERY);
1284 setUserQuery(query);
Mike LeBeau35df87c2009-06-24 13:06:39 -07001285 mSearchAutoComplete.showDropDown();
Karl Rosaen875d50a2009-04-23 19:00:21 -07001286 }
Karl Rosaena058f022009-06-01 23:11:44 +01001287
Karl Rosaen875d50a2009-04-23 19:00:21 -07001288 /**
Mike LeBeauae9760b2009-06-01 21:53:09 +01001289 * Sets the list item selection in the AutoCompleteTextView's ListView.
1290 */
1291 public void setListSelection(int index) {
1292 mSearchAutoComplete.setListSelection(index);
1293 }
Karl Rosaena058f022009-06-01 23:11:44 +01001294
Mike LeBeauae9760b2009-06-01 21:53:09 +01001295 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001296 * Saves the previous component that was searched, so that we can go
1297 * back to it.
1298 */
1299 private void pushPreviousComponent(ComponentName componentName) {
1300 if (mPreviousComponents == null) {
1301 mPreviousComponents = new ArrayList<ComponentName>();
1302 }
1303 mPreviousComponents.add(componentName);
1304 }
1305
1306 /**
1307 * Pops the previous component off the stack and returns it.
1308 *
1309 * @return The component name, or <code>null</code> if there was
1310 * no previous component.
1311 */
1312 private ComponentName popPreviousComponent() {
1313 if (mPreviousComponents == null) {
1314 return null;
1315 }
1316 int size = mPreviousComponents.size();
1317 if (size == 0) {
1318 return null;
1319 }
1320 return mPreviousComponents.remove(size - 1);
1321 }
1322
1323 /**
1324 * Goes back to the previous component that was searched, if any.
1325 *
1326 * @return <code>true</code> if there was a previous component that we could go back to.
1327 */
1328 private boolean backToPreviousComponent() {
1329 ComponentName previous = popPreviousComponent();
1330 if (previous == null) {
1331 return false;
1332 }
1333 if (!show(previous, mAppSearchData, false)) {
1334 Log.w(LOG_TAG, "Failed to switch to source " + previous);
1335 return false;
1336 }
1337
1338 // must touch text to trigger suggestions
1339 // TODO: should this be the text as it was when the user left
1340 // the source that we are now going back to?
1341 String query = mSearchAutoComplete.getText().toString();
1342 setUserQuery(query);
1343
1344 return true;
1345 }
1346
1347 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001348 * When a particular suggestion has been selected, perform the various lookups required
1349 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
1350 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
1351 * the suggestion includes a data id.
1352 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001353 * @param c The suggestions cursor, moved to the row of the user's selection
Karl Rosaen875d50a2009-04-23 19:00:21 -07001354 * @param actionKey The key code of the action key that was pressed,
1355 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1356 * @param actionMsg The message for the action key that was pressed,
1357 * or <code>null</code> if none.
1358 * @return An intent for the suggestion at the cursor's position.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001359 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001360 private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001361 try {
1362 // use specific action if supplied, or default action if supplied, or fixed default
Karl Rosaen875d50a2009-04-23 19:00:21 -07001363 String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
Karl Rosaena058f022009-06-01 23:11:44 +01001364
1365 // some items are display only, or have effect via the cursor respond click reporting.
1366 if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
1367 return null;
1368 }
1369
Karl Rosaen875d50a2009-04-23 19:00:21 -07001370 if (action == null) {
1371 action = mSearchable.getSuggestIntentAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001372 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001373 if (action == null) {
1374 action = Intent.ACTION_SEARCH;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001375 }
1376
1377 // use specific data if supplied, or default data if supplied
Karl Rosaen875d50a2009-04-23 19:00:21 -07001378 String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001379 if (data == null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001380 data = mSearchable.getSuggestIntentData();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001381 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001382 // then, if an ID was provided, append it.
1383 if (data != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001384 String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
1385 if (id != null) {
1386 data = data + "/" + Uri.encode(id);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001387 }
1388 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001389 Uri dataUri = (data == null) ? null : Uri.parse(data);
1390
Satish Sampathbf23fe02009-06-15 23:47:56 +01001391 String componentName = getColumnString(
1392 c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
Karl Rosaena058f022009-06-01 23:11:44 +01001393
Karl Rosaen875d50a2009-04-23 19:00:21 -07001394 String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
Satish Sampathbf23fe02009-06-15 23:47:56 +01001395 String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001396
Satish Sampathbf23fe02009-06-15 23:47:56 +01001397 return createIntent(action, dataUri, extraData, query, componentName, actionKey,
1398 actionMsg);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001399 } catch (RuntimeException e ) {
1400 int rowNum;
1401 try { // be really paranoid now
1402 rowNum = c.getPosition();
1403 } catch (RuntimeException e2 ) {
1404 rowNum = -1;
1405 }
1406 Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
1407 " returned exception" + e.toString());
Karl Rosaen875d50a2009-04-23 19:00:21 -07001408 return null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001409 }
1410 }
1411
1412 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001413 * Constructs an intent from the given information and the search dialog state.
1414 *
1415 * @param action Intent action.
1416 * @param data Intent data, or <code>null</code>.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001417 * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
Satish Sampathbf23fe02009-06-15 23:47:56 +01001418 * @param query Intent query, or <code>null</code>.
1419 * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
Karl Rosaen875d50a2009-04-23 19:00:21 -07001420 * @param actionKey The key code of the action key that was pressed,
1421 * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
1422 * @param actionMsg The message for the action key that was pressed,
1423 * or <code>null</code> if none.
1424 * @return The intent.
1425 */
Satish Sampathbf23fe02009-06-15 23:47:56 +01001426 private Intent createIntent(String action, Uri data, String extraData, String query,
1427 String componentName, int actionKey, String actionMsg) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001428 // Now build the Intent
1429 Intent intent = new Intent(action);
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001430 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001431 if (data != null) {
1432 intent.setData(data);
1433 }
Bjorn Bringert5f806052009-06-24 12:02:26 +01001434 intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001435 if (query != null) {
1436 intent.putExtra(SearchManager.QUERY, query);
1437 }
1438 if (extraData != null) {
1439 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
1440 }
Satish Sampathbf23fe02009-06-15 23:47:56 +01001441 if (componentName != null) {
1442 intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName);
1443 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001444 if (mAppSearchData != null) {
1445 intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
1446 }
1447 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
1448 intent.putExtra(SearchManager.ACTION_KEY, actionKey);
1449 intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
1450 }
1451 // attempt to enforce security requirement (no 3rd-party intents)
Bjorn Bringerta9204132009-05-05 14:06:35 +01001452 intent.setComponent(mSearchable.getSearchActivity());
Karl Rosaen875d50a2009-04-23 19:00:21 -07001453 return intent;
1454 }
1455
1456 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001457 * For a given suggestion and a given cursor row, get the action message. If not provided
1458 * by the specific row/column, also check for a single definition (for the action key).
1459 *
1460 * @param c The cursor providing suggestions
1461 * @param actionKey The actionkey record being examined
1462 *
1463 * @return Returns a string, or null if no action key message for this suggestion
1464 */
Karl Rosaen875d50a2009-04-23 19:00:21 -07001465 private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001466 String result = null;
1467 // check first in the cursor data, for a suggestion-specific message
Bjorn Bringerta9204132009-05-05 14:06:35 +01001468 final String column = actionKey.getSuggestActionMsgColumn();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001469 if (column != null) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001470 result = SuggestionsAdapter.getColumnString(c, column);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001471 }
1472 // If the cursor didn't give us a message, see if there's a single message defined
1473 // for the actionkey (for all suggestions)
1474 if (result == null) {
Bjorn Bringerta9204132009-05-05 14:06:35 +01001475 result = actionKey.getSuggestActionMsg();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001476 }
1477 return result;
1478 }
1479
1480 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001481 * Local subclass for AutoCompleteTextView.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001482 */
1483 public static class SearchAutoComplete extends AutoCompleteTextView {
1484
Karl Rosaen875d50a2009-04-23 19:00:21 -07001485 private int mThreshold;
1486 private SearchDialog mSearchDialog;
1487
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001488 public SearchAutoComplete(Context context) {
Marco Nelissen1746d6f2009-05-14 13:29:24 -07001489 super(context);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001490 mThreshold = getThreshold();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001491 }
1492
1493 public SearchAutoComplete(Context context, AttributeSet attrs) {
1494 super(context, attrs);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001495 mThreshold = getThreshold();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001496 }
1497
1498 public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
1499 super(context, attrs, defStyle);
Karl Rosaen875d50a2009-04-23 19:00:21 -07001500 mThreshold = getThreshold();
1501 }
1502
1503 private void setSearchDialog(SearchDialog searchDialog) {
1504 mSearchDialog = searchDialog;
1505 }
1506
1507 @Override
1508 public void setThreshold(int threshold) {
1509 super.setThreshold(threshold);
1510 mThreshold = threshold;
1511 }
1512
1513 /**
1514 * Returns true if the text field is empty, or contains only whitespace.
1515 */
1516 private boolean isEmpty() {
1517 return TextUtils.getTrimmedLength(getText()) == 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001518 }
Bjorn Bringert8d17f3f2009-06-05 13:22:28 +01001519
Karl Rosaen875d50a2009-04-23 19:00:21 -07001520 /**
1521 * We override this method to avoid replacing the query box text
1522 * when a suggestion is clicked.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001523 */
1524 @Override
Karl Rosaen875d50a2009-04-23 19:00:21 -07001525 protected void replaceText(CharSequence text) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001526 }
1527
1528 /**
Mike LeBeau617202a2009-07-06 14:29:25 -07001529 * We override this method to avoid an extra onItemClick being called on the
1530 * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)}
1531 * when an item is clicked with the trackball.
1532 */
1533 @Override
1534 public void performCompletion() {
1535 }
Mike LeBeau1fffbd92009-07-14 15:57:41 -07001536
1537 /**
1538 * We override this method to be sure and show the soft keyboard if appropriate when
1539 * the TextView has focus.
1540 */
1541 @Override
1542 public void onWindowFocusChanged(boolean hasWindowFocus) {
1543 super.onWindowFocusChanged(hasWindowFocus);
1544
1545 if (hasWindowFocus) {
1546 InputMethodManager inputManager = (InputMethodManager)
1547 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
1548 inputManager.showSoftInput(this, 0);
1549 }
1550 }
1551
Mike LeBeau617202a2009-07-06 14:29:25 -07001552 /**
Karl Rosaen875d50a2009-04-23 19:00:21 -07001553 * 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 -08001554 */
1555 @Override
1556 public boolean enoughToFilter() {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001557 return mThreshold <= 0 || super.enoughToFilter();
1558 }
Karl Rosaen98e333f2009-04-28 10:39:09 -07001559
Karl Rosaen875d50a2009-04-23 19:00:21 -07001560 /**
1561 * {@link AutoCompleteTextView#onKeyPreIme(int, KeyEvent)}) dismisses the drop-down on BACK,
1562 * so we must override this method to modify the BACK behavior.
1563 */
1564 @Override
1565 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
Karl Rosaen98e333f2009-04-28 10:39:09 -07001566 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
1567 if (mSearchDialog.backToPreviousComponent()) {
1568 return true;
1569 }
1570 return false; // will dismiss soft keyboard if necessary
1571 }
1572 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001573 }
1574 }
1575
Karl Rosaen875d50a2009-04-23 19:00:21 -07001576 protected boolean handleBackKey(int keyCode, KeyEvent event) {
1577 if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001578 if (backToPreviousComponent()) {
1579 return true;
1580 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001581 cancel();
1582 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001583 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001584 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001585 }
1586
1587 /**
1588 * Implements OnItemClickListener
1589 */
1590 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001591 if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
1592 launchSuggestion(position);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001593 }
Karl Rosaen875d50a2009-04-23 19:00:21 -07001594
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001595 /**
1596 * Implements OnItemSelectedListener
1597 */
1598 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001599 if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
1600 // A suggestion has been selected, rewrite the query if possible,
1601 // otherwise the restore the original query.
1602 if (REWRITE_QUERIES) {
1603 rewriteQueryFromSuggestion(position);
1604 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001605 }
1606
1607 /**
1608 * Implements OnItemSelectedListener
1609 */
1610 public void onNothingSelected(AdapterView<?> parent) {
Karl Rosaen875d50a2009-04-23 19:00:21 -07001611 if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
1612 }
1613
1614 /**
1615 * Query rewriting.
1616 */
1617
1618 private void rewriteQueryFromSuggestion(int position) {
1619 Cursor c = mSuggestionsAdapter.getCursor();
1620 if (c == null) {
1621 return;
1622 }
1623 if (c.moveToPosition(position)) {
1624 // Get the new query from the suggestion.
1625 CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
1626 if (newQuery != null) {
1627 // The suggestion rewrites the query.
1628 if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
1629 // Update the text field, without getting new suggestions.
1630 setQuery(newQuery);
1631 } else {
1632 // The suggestion does not rewrite the query, restore the user's query.
1633 if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
1634 restoreUserQuery();
1635 }
1636 } else {
1637 // We got a bad position, restore the user's query.
1638 Log.w(LOG_TAG, "Bad suggestion position: " + position);
1639 restoreUserQuery();
1640 }
1641 }
1642
1643 /**
1644 * Restores the query entered by the user if needed.
1645 */
1646 private void restoreUserQuery() {
1647 if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
1648 setQuery(mUserQuery);
1649 }
1650
1651 /**
1652 * Sets the text in the query box, without updating the suggestions.
1653 */
1654 private void setQuery(CharSequence query) {
1655 mSearchAutoComplete.setText(query, false);
1656 if (query != null) {
1657 mSearchAutoComplete.setSelection(query.length());
1658 }
1659 }
1660
1661 /**
1662 * Sets the text in the query box, updating the suggestions.
1663 */
1664 private void setUserQuery(String query) {
1665 if (query == null) {
1666 query = "";
1667 }
1668 mUserQuery = query;
1669 mSearchAutoComplete.setText(query);
1670 mSearchAutoComplete.setSelection(query.length());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001671 }
1672
1673 /**
1674 * Debugging Support
1675 */
1676
1677 /**
1678 * For debugging only, sample the millisecond clock and log it.
1679 * Uses AtomicLong so we can use in multiple threads
1680 */
1681 private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis());
1682 private void dbgLogTiming(final String caller) {
1683 long millis = SystemClock.uptimeMillis();
1684 long oldTime = mLastLogTime.getAndSet(millis);
1685 long delta = millis - oldTime;
1686 final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller;
1687 Log.d(LOG_TAG,report);
1688 }
1689}