blob: 522a4326202d4383f2cc76ac49886951421d3ac3 [file] [log] [blame]
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -07001/*
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 com.android.launcher;
18
19import android.app.ISearchManager;
20import android.app.SearchManager;
The Android Open Source Projectd097a182008-12-17 18:05:58 -080021import android.content.ActivityNotFoundException;
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070022import android.content.Context;
23import android.content.Intent;
The Android Open Source Projectd097a182008-12-17 18:05:58 -080024import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070026import android.content.res.Resources;
27import android.content.res.Resources.NotFoundException;
28import android.database.Cursor;
The Android Open Source Projectd097a182008-12-17 18:05:58 -080029import android.graphics.Rect;
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070030import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.server.search.SearchableInfo;
36import android.text.Editable;
37import android.text.TextUtils;
38import android.text.TextWatcher;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.view.KeyEvent;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.ViewGroup;
45import android.view.View.OnClickListener;
46import android.view.View.OnKeyListener;
47import android.view.View.OnLongClickListener;
48import android.widget.AdapterView;
49import android.widget.AutoCompleteTextView;
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070050import android.widget.CursorAdapter;
51import android.widget.Filter;
The Android Open Source Projectd097a182008-12-17 18:05:58 -080052import android.widget.ImageButton;
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070053import android.widget.ImageView;
54import android.widget.LinearLayout;
55import android.widget.SimpleCursorAdapter;
56import android.widget.TextView;
57import android.widget.AdapterView.OnItemClickListener;
58import android.widget.AdapterView.OnItemSelectedListener;
59
60public class Search extends LinearLayout implements OnClickListener, OnKeyListener,
61 OnLongClickListener, TextWatcher, OnItemClickListener, OnItemSelectedListener {
62
63 private final String TAG = "SearchGadget";
64
65 private AutoCompleteTextView mSearchText;
The Android Open Source Projectd097a182008-12-17 18:05:58 -080066 private ImageButton mGoButton;
The Android Open Source Project15a88802009-02-10 15:44:05 -080067 private ImageButton mVoiceButton;
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070068 private OnLongClickListener mLongClickListener;
69
70 // Support for suggestions
71 private SuggestionsAdapter mSuggestionsAdapter;
72 private SearchableInfo mSearchable;
73 private String mSuggestionAction = null;
74 private Uri mSuggestionData = null;
75 private String mSuggestionQuery = null;
76 private int mItemSelected = -1;
The Android Open Source Projectd097a182008-12-17 18:05:58 -080077
The Android Open Source Project15a88802009-02-10 15:44:05 -080078 // For voice searching
79 private Intent mVoiceSearchIntent;
80
The Android Open Source Projectd097a182008-12-17 18:05:58 -080081 private Rect mTempRect = new Rect();
The Android Open Source Projectb28e1b72009-03-02 22:54:41 -080082 private boolean mRestoreFocus = false;
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070083
84 /**
85 * Used to inflate the Workspace from XML.
86 *
87 * @param context The application's context.
The Android Open Source Projectd097a182008-12-17 18:05:58 -080088 * @param attrs The attributes set containing the Workspace's customization values.
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070089 */
90 public Search(Context context, AttributeSet attrs) {
91 super(context, attrs);
The Android Open Source Project15a88802009-02-10 15:44:05 -080092
93 mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
94 mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL,
95 android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -070096 }
97
98 /**
99 * Implements OnClickListener (for button)
100 */
101 public void onClick(View v) {
The Android Open Source Projectd097a182008-12-17 18:05:58 -0800102 if (v == mGoButton) {
103 query();
The Android Open Source Project15a88802009-02-10 15:44:05 -0800104 } else if (v == mVoiceButton) {
105 try {
106 getContext().startActivity(mVoiceSearchIntent);
107 } catch (ActivityNotFoundException ex) {
108 // Should not happen, since we check the availability of
109 // voice search before showing the button. But just in case...
110 Log.w(TAG, "Could not find voice search activity");
111 }
The Android Open Source Projectd097a182008-12-17 18:05:58 -0800112 }
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700113 }
114
115 private void query() {
116 String query = mSearchText.getText().toString();
117 if (TextUtils.getTrimmedLength(mSearchText.getText()) == 0) {
118 return;
119 }
The Android Open Source Projectd097a182008-12-17 18:05:58 -0800120 Bundle appData = new Bundle();
121 appData.putString(SearchManager.SOURCE, "launcher-widget");
122 sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, 0, null, mSearchable);
The Android Open Source Project946cd912009-01-15 16:12:13 -0800123 clearQuery();
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700124 }
125
126 /**
127 * Assemble a search intent and send it.
128 *
129 * This is copied from SearchDialog.
130 *
131 * @param action The intent to send, typically Intent.ACTION_SEARCH
132 * @param data The data for the intent
133 * @param query The user text entered (so far)
134 * @param appData The app data bundle (if supplied)
135 * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will
136 * be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
137 * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the
138 * corresponding tag message will be sent here. Pass null for no actionKey message.
139 * @param si Reference to the current SearchableInfo. Passed here so it can be used even after
140 * we've called dismiss(), which attempts to null mSearchable.
141 */
142 private void sendLaunchIntent(final String action, final Uri data, final String query,
143 final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) {
144 Intent launcher = new Intent(action);
145 launcher.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
146
147 if (query != null) {
148 launcher.putExtra(SearchManager.QUERY, query);
149 }
150
151 if (data != null) {
152 launcher.setData(data);
153 }
154
155 if (appData != null) {
156 launcher.putExtra(SearchManager.APP_DATA, appData);
157 }
158
159 // add launch info (action key, etc.)
160 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
161 launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
162 launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
163 }
164
165 // attempt to enforce security requirement (no 3rd-party intents)
166 if (si != null) {
167 launcher.setComponent(si.mSearchActivity);
168 }
169
170 getContext().startActivity(launcher);
171 }
The Android Open Source Projectb28e1b72009-03-02 22:54:41 -0800172
173 @Override
174 public void onWindowFocusChanged(boolean hasWindowFocus) {
175 if (!hasWindowFocus && hasFocus()) {
176 mRestoreFocus = true;
177 }
178
179 super.onWindowFocusChanged(hasWindowFocus);
180
181 if (hasWindowFocus && mRestoreFocus) {
182 if (isInTouchMode()) {
183 final AutoCompleteTextView searchText = mSearchText;
184 searchText.setSelectAllOnFocus(false);
185 searchText.requestFocusFromTouch();
186 searchText.setSelectAllOnFocus(true);
187 }
188 mRestoreFocus = false;
189 }
190 }
191
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700192 /**
193 * Implements TextWatcher (for EditText)
194 */
195 public void beforeTextChanged(CharSequence s, int start, int before, int after) {
196 }
197
198 /**
199 * Implements TextWatcher (for EditText)
200 */
201 public void onTextChanged(CharSequence s, int start, int before, int after) {
202 // enable the button if we have one or more non-space characters
203 boolean enabled = TextUtils.getTrimmedLength(mSearchText.getText()) != 0;
204 mGoButton.setEnabled(enabled);
205 mGoButton.setFocusable(enabled);
206 }
207
208 /**
209 * Implements TextWatcher (for EditText)
210 */
211 public void afterTextChanged(Editable s) {
212 }
213
214 /**
215 * Implements OnKeyListener (for EditText and for button)
216 *
217 * This plays some games with state in order to "soften" the strength of suggestions
218 * presented. Suggestions should not be used unless the user specifically navigates to them
219 * (or clicks them, in which case it's obvious). This is not the way that AutoCompleteTextBox
220 * normally works.
221 */
222 public final boolean onKey(View v, int keyCode, KeyEvent event) {
223 if (v == mSearchText) {
224 boolean searchTrigger = (keyCode == KeyEvent.KEYCODE_ENTER ||
225 keyCode == KeyEvent.KEYCODE_SEARCH ||
226 keyCode == KeyEvent.KEYCODE_DPAD_CENTER);
227 if (event.getAction() == KeyEvent.ACTION_UP) {
228// Log.d(TAG, "onKey() ACTION_UP isPopupShowing:" + mSearchText.isPopupShowing());
229 if (!mSearchText.isPopupShowing()) {
230 if (searchTrigger) {
231 query();
232 return true;
233 }
234 }
235 } else {
236// Log.d(TAG, "onKey() ACTION_DOWN isPopupShowing:" + mSearchText.isPopupShowing() +
237// " mItemSelected="+ mItemSelected);
238 if (searchTrigger && mItemSelected < 0) {
239 query();
240 return true;
241 }
242 }
The Android Open Source Project15a88802009-02-10 15:44:05 -0800243 } else if (v == mGoButton || v == mVoiceButton) {
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700244 boolean handled = false;
245 if (!event.isSystem() &&
246 (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
247 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
248 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
249 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
250 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
251 if (mSearchText.requestFocus()) {
252 handled = mSearchText.dispatchKeyEvent(event);
253 }
254 }
255 return handled;
256 }
257
258 return false;
259 }
260
261 @Override
262 public void setOnLongClickListener(OnLongClickListener l) {
263 super.setOnLongClickListener(l);
264 mLongClickListener = l;
265 }
266
267 /**
268 * Implements OnLongClickListener (for button)
269 */
270 public boolean onLongClick(View v) {
271 // Pretend that a long press on a child view is a long press on the search widget
272 if (mLongClickListener != null) {
273 return mLongClickListener.onLongClick(this);
274 }
275 return false;
276 }
277
278 @Override
279 public boolean onInterceptTouchEvent(MotionEvent ev) {
The Android Open Source Project15a88802009-02-10 15:44:05 -0800280 // Request focus unless the user tapped on the voice search button
281 final int x = (int) ev.getX();
282 final int y = (int) ev.getY();
283 final Rect frame = mTempRect;
284 mVoiceButton.getHitRect(frame);
285 if (!frame.contains(x, y)) {
286 requestFocusFromTouch();
287 }
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700288 return super.onInterceptTouchEvent(ev);
289 }
290
291 /**
292 * In order to keep things simple, the external trigger will clear the query just before
293 * focusing, so as to give you a fresh query. This way we eliminate any sources of
294 * accidental query launching.
295 */
296 public void clearQuery() {
297 mSearchText.setText(null);
298 }
299
300 @Override
301 protected void onFinishInflate() {
302 super.onFinishInflate();
303
304 mSearchText = (AutoCompleteTextView) findViewById(R.id.input);
305 // TODO: This can be confusing when the user taps the text field to give the focus
306 // (it is not necessary but I ran into this issue several times myself)
307 // mTitleInput.setOnClickListener(this);
308 mSearchText.setOnKeyListener(this);
309 mSearchText.addTextChangedListener(this);
310
The Android Open Source Projectd097a182008-12-17 18:05:58 -0800311 mGoButton = (ImageButton) findViewById(R.id.search_go_btn);
The Android Open Source Project15a88802009-02-10 15:44:05 -0800312 mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn);
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700313 mGoButton.setOnClickListener(this);
The Android Open Source Project15a88802009-02-10 15:44:05 -0800314 mVoiceButton.setOnClickListener(this);
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700315 mGoButton.setOnKeyListener(this);
The Android Open Source Project15a88802009-02-10 15:44:05 -0800316 mVoiceButton.setOnKeyListener(this);
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700317
318 mSearchText.setOnLongClickListener(this);
319 mGoButton.setOnLongClickListener(this);
The Android Open Source Project15a88802009-02-10 15:44:05 -0800320 mVoiceButton.setOnLongClickListener(this);
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700321
322 // disable the button since we start out w/empty input
323 mGoButton.setEnabled(false);
324 mGoButton.setFocusable(false);
325
The Android Open Source Project15a88802009-02-10 15:44:05 -0800326 configureSearchableInfo();
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700327 configureSuggestions();
The Android Open Source Project15a88802009-02-10 15:44:05 -0800328 configureVoiceSearchButton();
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700329 }
330
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700331 /**
The Android Open Source Project15a88802009-02-10 15:44:05 -0800332 * Read the searchable info from the search manager
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700333 */
The Android Open Source Project15a88802009-02-10 15:44:05 -0800334 private void configureSearchableInfo() {
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700335 ISearchManager sms;
336 SearchableInfo searchable;
337 sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE));
338 try {
339 // TODO null isn't the published use of this API, but it works when global=true
340 // TODO better implementation: defer all of this, let Home set it up
341 searchable = sms.getSearchableInfo(null, true);
342 } catch (RemoteException e) {
343 searchable = null;
344 }
345 if (searchable == null) {
346 // no suggestions so just get out (no need to continue)
347 return;
348 }
349 mSearchable = searchable;
The Android Open Source Project15a88802009-02-10 15:44:05 -0800350 }
351
352 /**
353 * If appropriate & available, configure voice search
354 *
355 * Note: Because the home screen search widget is always web search, we only check for
356 * getVoiceSearchLaunchWebSearch() modes. We don't support the alternate form of app-specific
357 * voice search.
358 */
359 private void configureVoiceSearchButton() {
360 boolean voiceSearchVisible = false;
361 if (mSearchable.getVoiceSearchEnabled() && mSearchable.getVoiceSearchLaunchWebSearch()) {
362 // Enable the voice search button if there is an activity that can handle it
363 PackageManager pm = getContext().getPackageManager();
The Android Open Source Project233a0132009-02-19 10:57:35 -0800364 ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
The Android Open Source Project15a88802009-02-10 15:44:05 -0800365 PackageManager.MATCH_DEFAULT_ONLY);
The Android Open Source Project233a0132009-02-19 10:57:35 -0800366 voiceSearchVisible = ri != null;
The Android Open Source Project15a88802009-02-10 15:44:05 -0800367 }
368
369 // finally, set visible state of voice search button, as appropriate
370 mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE);
371 }
372
373 /** The rest of the class deals with providing search suggestions */
374
375 /**
376 * Set up the suggestions provider mechanism
377 */
378 private void configureSuggestions() {
379 // get SearchableInfo
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700380
381 mSearchText.setOnItemClickListener(this);
382 mSearchText.setOnItemSelectedListener(this);
383
384 // attach the suggestions adapter
385 mSuggestionsAdapter = new SuggestionsAdapter(mContext,
The Android Open Source Projectd097a182008-12-17 18:05:58 -0800386 com.android.internal.R.layout.search_dropdown_item_2line, null,
387 SuggestionsAdapter.TWO_LINE_FROM, SuggestionsAdapter.TWO_LINE_TO, mSearchable);
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700388 mSearchText.setAdapter(mSuggestionsAdapter);
389 }
390
391 /**
The Android Open Source Projectb28e1b72009-03-02 22:54:41 -0800392 * Remove internal cursor references when detaching from window which
393 * prevents {@link Context} leaks.
394 */
395 @Override
396 public void onDetachedFromWindow() {
397 if (mSuggestionsAdapter != null) {
398 mSuggestionsAdapter.changeCursor(null);
399 mSuggestionsAdapter = null;
400 }
401 }
402
403 /**
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700404 * Implements OnItemClickListener
405 */
406 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
407// Log.d(TAG, "onItemClick() position " + position);
408 launchSuggestion(mSuggestionsAdapter, position);
409 }
410
411 /**
412 * Implements OnItemSelectedListener
413 */
414 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
415// Log.d(TAG, "onItemSelected() position " + position);
416 mItemSelected = position;
417 }
418
419 /**
420 * Implements OnItemSelectedListener
421 */
422 public void onNothingSelected(AdapterView<?> parent) {
423// Log.d(TAG, "onNothingSelected()");
424 mItemSelected = -1;
425 }
426
427 /**
428 * Code to launch a suggestion query.
429 *
430 * This is copied from SearchDialog.
431 *
432 * @param ca The CursorAdapter containing the suggestions
433 * @param position The suggestion we'll be launching from
434 *
435 * @return Returns true if a successful launch, false if could not (e.g. bad position)
436 */
437 private boolean launchSuggestion(CursorAdapter ca, int position) {
438 if (ca != null) {
439 Cursor c = ca.getCursor();
440 if ((c != null) && c.moveToPosition(position)) {
441 setupSuggestionIntent(c, mSearchable);
442
443 SearchableInfo si = mSearchable;
444 String suggestionAction = mSuggestionAction;
445 Uri suggestionData = mSuggestionData;
446 String suggestionQuery = mSuggestionQuery;
447 sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, null,
448 KeyEvent.KEYCODE_UNKNOWN, null, si);
The Android Open Source Project946cd912009-01-15 16:12:13 -0800449 clearQuery();
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700450 return true;
451 }
452 }
453 return false;
454 }
455
456 /**
457 * When a particular suggestion has been selected, perform the various lookups required
458 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
459 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
460 * the suggestion includes a data id.
461 *
462 * NOTE: Return values are in member variables mSuggestionAction, mSuggestionData and
463 * mSuggestionQuery.
464 *
465 * This is copied from SearchDialog.
466 *
467 * @param c The suggestions cursor, moved to the row of the user's selection
468 * @param si The searchable activity's info record
469 */
470 void setupSuggestionIntent(Cursor c, SearchableInfo si) {
471 try {
472 // use specific action if supplied, or default action if supplied, or fixed default
473 mSuggestionAction = null;
474 int column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
475 if (column >= 0) {
476 final String action = c.getString(column);
477 if (action != null) {
478 mSuggestionAction = action;
479 }
480 }
481 if (mSuggestionAction == null) {
482 mSuggestionAction = si.getSuggestIntentAction();
483 }
484 if (mSuggestionAction == null) {
485 mSuggestionAction = Intent.ACTION_SEARCH;
486 }
487
488 // use specific data if supplied, or default data if supplied
489 String data = null;
490 column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
491 if (column >= 0) {
492 final String rowData = c.getString(column);
493 if (rowData != null) {
494 data = rowData;
495 }
496 }
497 if (data == null) {
498 data = si.getSuggestIntentData();
499 }
500
501 // then, if an ID was provided, append it.
502 if (data != null) {
503 column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
504 if (column >= 0) {
505 final String id = c.getString(column);
506 if (id != null) {
507 data = data + "/" + Uri.encode(id);
508 }
509 }
510 }
511 mSuggestionData = (data == null) ? null : Uri.parse(data);
512
513 mSuggestionQuery = null;
514 column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
515 if (column >= 0) {
516 final String query = c.getString(column);
517 if (query != null) {
518 mSuggestionQuery = query;
519 }
520 }
521 } catch (RuntimeException e ) {
522 int rowNum;
523 try { // be really paranoid now
524 rowNum = c.getPosition();
525 } catch (RuntimeException e2 ) {
526 rowNum = -1;
527 }
528 Log.w(TAG, "Search Suggestions cursor at row " + rowNum +
529 " returned exception" + e.toString());
530 }
531 }
532
533 /**
534 * This class provides the filtering-based interface to suggestions providers.
The Android Open Source Projectd097a182008-12-17 18:05:58 -0800535 * It is hardwired in a couple of places to support GoogleSearch - for example, it supports
536 * two-line suggestions, but it does not support icons.
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700537 */
538 private static class SuggestionsAdapter extends SimpleCursorAdapter {
The Android Open Source Projectd097a182008-12-17 18:05:58 -0800539 public final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
540 SearchManager.SUGGEST_COLUMN_TEXT_2 };
541 public final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1,
542 com.android.internal.R.id.text2};
The Android Open Source Projectc8f00b62008-10-21 07:00:00 -0700543
544 private final String TAG = "SuggestionsAdapter";
545
546 Filter mFilter;
547 SearchableInfo mSearchable;
548 private Resources mProviderResources;
549 String[] mFromStrings;
550
551 public SuggestionsAdapter(Context context, int layout, Cursor c,
552 String[] from, int[] to, SearchableInfo searchable) {
553 super(context, layout, c, from, to);
554 mFromStrings = from;
555 mSearchable = searchable;
556
557 // set up provider resources (gives us icons, etc.)
558 Context activityContext = mSearchable.getActivityContext(mContext);
559 Context providerContext = mSearchable.getProviderContext(mContext, activityContext);
560 mProviderResources = providerContext.getResources();
561 }
562
563 /**
564 * Use the search suggestions provider to obtain a live cursor. This will be called
565 * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
566 * The results will be processed in the UI thread and changeCursor() will be called.
567 */
568 @Override
569 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
570 String query = (constraint == null) ? "" : constraint.toString();
571 return getSuggestions(mSearchable, query);
572 }
573
574 /**
575 * Overriding this allows us to write the selected query back into the box.
576 * NOTE: This is a vastly simplified version of SearchDialog.jamQuery() and does
577 * not universally support the search API. But it is sufficient for Google Search.
578 */
579 @Override
580 public CharSequence convertToString(Cursor cursor) {
581 CharSequence result = null;
582 if (cursor != null) {
583 int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
584 if (column >= 0) {
585 final String query = cursor.getString(column);
586 if (query != null) {
587 result = query;
588 }
589 }
590 }
591 return result;
592 }
593
594 /**
595 * Get the query cursor for the search suggestions.
596 *
597 * TODO this is functionally identical to the version in SearchDialog.java. Perhaps it
598 * could be hoisted into SearchableInfo or some other shared spot.
599 *
600 * @param query The search text entered (so far)
601 * @return Returns a cursor with suggestions, or null if no suggestions
602 */
603 private Cursor getSuggestions(final SearchableInfo searchable, final String query) {
604 Cursor cursor = null;
605 if (searchable.getSuggestAuthority() != null) {
606 try {
607 StringBuilder uriStr = new StringBuilder("content://");
608 uriStr.append(searchable.getSuggestAuthority());
609
610 // if content path provided, insert it now
611 final String contentPath = searchable.getSuggestPath();
612 if (contentPath != null) {
613 uriStr.append('/');
614 uriStr.append(contentPath);
615 }
616
617 // append standard suggestion query path
618 uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY);
619
620 // inject query, either as selection args or inline
621 String[] selArgs = null;
622 if (searchable.getSuggestSelection() != null) { // use selection if provided
623 selArgs = new String[] {query};
624 } else {
625 uriStr.append('/'); // no sel, use REST pattern
626 uriStr.append(Uri.encode(query));
627 }
628
629 // finally, make the query
630 cursor = mContext.getContentResolver().query(
631 Uri.parse(uriStr.toString()), null,
632 searchable.getSuggestSelection(), selArgs,
633 null);
634 } catch (RuntimeException e) {
635 Log.w(TAG, "Search Suggestions query returned exception " + e.toString());
636 cursor = null;
637 }
638 }
639
640 return cursor;
641 }
642
643 /**
644 * Overriding this allows us to affect the way that an icon is loaded. Specifically,
645 * we can be more controlling about the resource path (and allow icons to come from other
646 * packages).
647 *
648 * TODO: This is 100% identical to the version in SearchDialog.java
649 *
650 * @param v ImageView to receive an image
651 * @param value the value retrieved from the cursor
652 */
653 @Override
654 public void setViewImage(ImageView v, String value) {
655 int resID;
656 Drawable img = null;
657
658 try {
659 resID = Integer.parseInt(value);
660 if (resID != 0) {
661 img = mProviderResources.getDrawable(resID);
662 }
663 } catch (NumberFormatException nfe) {
664 // img = null;
665 } catch (NotFoundException e2) {
666 // img = null;
667 }
668
669 // finally, set the image to whatever we've gotten
670 v.setImageDrawable(img);
671 }
672
673 /**
674 * This method is overridden purely to provide a bit of protection against
675 * flaky content providers.
676 *
677 * TODO: This is 100% identical to the version in SearchDialog.java
678 *
679 * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
680 */
681 @Override
682 public View getView(int position, View convertView, ViewGroup parent) {
683 try {
684 return super.getView(position, convertView, parent);
685 } catch (RuntimeException e) {
686 Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString());
687 // what can I return here?
688 View v = newView(mContext, mCursor, parent);
689 if (v != null) {
690 TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1);
691 tv.setText(e.toString());
692 }
693 return v;
694 }
695 }
696
697 }
698}