blob: 69e26ac59da632365ce0033fa4037a8fe0ae877c [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;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.content.res.Resources.NotFoundException;
25import android.database.Cursor;
26import android.graphics.drawable.Drawable;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.RemoteException;
30import android.os.ServiceManager;
31import android.server.search.SearchableInfo;
32import android.text.Editable;
33import android.text.TextUtils;
34import android.text.TextWatcher;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.view.KeyEvent;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.ViewGroup;
41import android.view.View.OnClickListener;
42import android.view.View.OnKeyListener;
43import android.view.View.OnLongClickListener;
44import android.widget.AdapterView;
45import android.widget.AutoCompleteTextView;
46import android.widget.Button;
47import android.widget.CursorAdapter;
48import android.widget.Filter;
49import android.widget.ImageView;
50import android.widget.LinearLayout;
51import android.widget.SimpleCursorAdapter;
52import android.widget.TextView;
53import android.widget.AdapterView.OnItemClickListener;
54import android.widget.AdapterView.OnItemSelectedListener;
55
56public class Search extends LinearLayout implements OnClickListener, OnKeyListener,
57 OnLongClickListener, TextWatcher, OnItemClickListener, OnItemSelectedListener {
58
59 private final String TAG = "SearchGadget";
60
61 private AutoCompleteTextView mSearchText;
62 private Button mGoButton;
63 private OnLongClickListener mLongClickListener;
64
65 // Support for suggestions
66 private SuggestionsAdapter mSuggestionsAdapter;
67 private SearchableInfo mSearchable;
68 private String mSuggestionAction = null;
69 private Uri mSuggestionData = null;
70 private String mSuggestionQuery = null;
71 private int mItemSelected = -1;
72
73 /**
74 * Used to inflate the Workspace from XML.
75 *
76 * @param context The application's context.
77 * @param attrs The attribtues set containing the Workspace's customization values.
78 */
79 public Search(Context context, AttributeSet attrs) {
80 super(context, attrs);
81 }
82
83 /**
84 * Implements OnClickListener (for button)
85 */
86 public void onClick(View v) {
87 query();
88 }
89
90 private void query() {
91 String query = mSearchText.getText().toString();
92 if (TextUtils.getTrimmedLength(mSearchText.getText()) == 0) {
93 return;
94 }
95 sendLaunchIntent(Intent.ACTION_SEARCH, null, query, null, 0, null, mSearchable);
96 }
97
98 /**
99 * Assemble a search intent and send it.
100 *
101 * This is copied from SearchDialog.
102 *
103 * @param action The intent to send, typically Intent.ACTION_SEARCH
104 * @param data The data for the intent
105 * @param query The user text entered (so far)
106 * @param appData The app data bundle (if supplied)
107 * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will
108 * be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
109 * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the
110 * corresponding tag message will be sent here. Pass null for no actionKey message.
111 * @param si Reference to the current SearchableInfo. Passed here so it can be used even after
112 * we've called dismiss(), which attempts to null mSearchable.
113 */
114 private void sendLaunchIntent(final String action, final Uri data, final String query,
115 final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) {
116 Intent launcher = new Intent(action);
117 launcher.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
118
119 if (query != null) {
120 launcher.putExtra(SearchManager.QUERY, query);
121 }
122
123 if (data != null) {
124 launcher.setData(data);
125 }
126
127 if (appData != null) {
128 launcher.putExtra(SearchManager.APP_DATA, appData);
129 }
130
131 // add launch info (action key, etc.)
132 if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
133 launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
134 launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
135 }
136
137 // attempt to enforce security requirement (no 3rd-party intents)
138 if (si != null) {
139 launcher.setComponent(si.mSearchActivity);
140 }
141
142 getContext().startActivity(launcher);
143 }
144
145 /**
146 * Implements TextWatcher (for EditText)
147 */
148 public void beforeTextChanged(CharSequence s, int start, int before, int after) {
149 }
150
151 /**
152 * Implements TextWatcher (for EditText)
153 */
154 public void onTextChanged(CharSequence s, int start, int before, int after) {
155 // enable the button if we have one or more non-space characters
156 boolean enabled = TextUtils.getTrimmedLength(mSearchText.getText()) != 0;
157 mGoButton.setEnabled(enabled);
158 mGoButton.setFocusable(enabled);
159 }
160
161 /**
162 * Implements TextWatcher (for EditText)
163 */
164 public void afterTextChanged(Editable s) {
165 }
166
167 /**
168 * Implements OnKeyListener (for EditText and for button)
169 *
170 * This plays some games with state in order to "soften" the strength of suggestions
171 * presented. Suggestions should not be used unless the user specifically navigates to them
172 * (or clicks them, in which case it's obvious). This is not the way that AutoCompleteTextBox
173 * normally works.
174 */
175 public final boolean onKey(View v, int keyCode, KeyEvent event) {
176 if (v == mSearchText) {
177 boolean searchTrigger = (keyCode == KeyEvent.KEYCODE_ENTER ||
178 keyCode == KeyEvent.KEYCODE_SEARCH ||
179 keyCode == KeyEvent.KEYCODE_DPAD_CENTER);
180 if (event.getAction() == KeyEvent.ACTION_UP) {
181// Log.d(TAG, "onKey() ACTION_UP isPopupShowing:" + mSearchText.isPopupShowing());
182 if (!mSearchText.isPopupShowing()) {
183 if (searchTrigger) {
184 query();
185 return true;
186 }
187 }
188 } else {
189// Log.d(TAG, "onKey() ACTION_DOWN isPopupShowing:" + mSearchText.isPopupShowing() +
190// " mItemSelected="+ mItemSelected);
191 if (searchTrigger && mItemSelected < 0) {
192 query();
193 return true;
194 }
195 }
196 } else if (v == mGoButton) {
197 boolean handled = false;
198 if (!event.isSystem() &&
199 (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
200 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
201 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
202 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
203 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
204 if (mSearchText.requestFocus()) {
205 handled = mSearchText.dispatchKeyEvent(event);
206 }
207 }
208 return handled;
209 }
210
211 return false;
212 }
213
214 @Override
215 public void setOnLongClickListener(OnLongClickListener l) {
216 super.setOnLongClickListener(l);
217 mLongClickListener = l;
218 }
219
220 /**
221 * Implements OnLongClickListener (for button)
222 */
223 public boolean onLongClick(View v) {
224 // Pretend that a long press on a child view is a long press on the search widget
225 if (mLongClickListener != null) {
226 return mLongClickListener.onLongClick(this);
227 }
228 return false;
229 }
230
231 @Override
232 public boolean onInterceptTouchEvent(MotionEvent ev) {
233 requestFocusFromTouch();
234 return super.onInterceptTouchEvent(ev);
235 }
236
237 /**
238 * In order to keep things simple, the external trigger will clear the query just before
239 * focusing, so as to give you a fresh query. This way we eliminate any sources of
240 * accidental query launching.
241 */
242 public void clearQuery() {
243 mSearchText.setText(null);
244 }
245
246 @Override
247 protected void onFinishInflate() {
248 super.onFinishInflate();
249
250 mSearchText = (AutoCompleteTextView) findViewById(R.id.input);
251 // TODO: This can be confusing when the user taps the text field to give the focus
252 // (it is not necessary but I ran into this issue several times myself)
253 // mTitleInput.setOnClickListener(this);
254 mSearchText.setOnKeyListener(this);
255 mSearchText.addTextChangedListener(this);
256
257 mGoButton = (Button) findViewById(R.id.go);
258 mGoButton.setOnClickListener(this);
259 mGoButton.setOnKeyListener(this);
260
261 mSearchText.setOnLongClickListener(this);
262 mGoButton.setOnLongClickListener(this);
263
264 // disable the button since we start out w/empty input
265 mGoButton.setEnabled(false);
266 mGoButton.setFocusable(false);
267
268 configureSuggestions();
269 }
270
271 /** The rest of the class deals with providing search suggestions */
272
273 /**
274 * Set up the suggestions provider mechanism
275 */
276 private void configureSuggestions() {
277 // get SearchableInfo
278 ISearchManager sms;
279 SearchableInfo searchable;
280 sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE));
281 try {
282 // TODO null isn't the published use of this API, but it works when global=true
283 // TODO better implementation: defer all of this, let Home set it up
284 searchable = sms.getSearchableInfo(null, true);
285 } catch (RemoteException e) {
286 searchable = null;
287 }
288 if (searchable == null) {
289 // no suggestions so just get out (no need to continue)
290 return;
291 }
292 mSearchable = searchable;
293
294 mSearchText.setOnItemClickListener(this);
295 mSearchText.setOnItemSelectedListener(this);
296
297 // attach the suggestions adapter
298 mSuggestionsAdapter = new SuggestionsAdapter(mContext,
299 com.android.internal.R.layout.search_dropdown_item_1line, null,
300 SuggestionsAdapter.ONE_LINE_FROM, SuggestionsAdapter.ONE_LINE_TO, mSearchable);
301 mSearchText.setAdapter(mSuggestionsAdapter);
302 }
303
304 /**
305 * Implements OnItemClickListener
306 */
307 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
308// Log.d(TAG, "onItemClick() position " + position);
309 launchSuggestion(mSuggestionsAdapter, position);
310 }
311
312 /**
313 * Implements OnItemSelectedListener
314 */
315 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
316// Log.d(TAG, "onItemSelected() position " + position);
317 mItemSelected = position;
318 }
319
320 /**
321 * Implements OnItemSelectedListener
322 */
323 public void onNothingSelected(AdapterView<?> parent) {
324// Log.d(TAG, "onNothingSelected()");
325 mItemSelected = -1;
326 }
327
328 /**
329 * Code to launch a suggestion query.
330 *
331 * This is copied from SearchDialog.
332 *
333 * @param ca The CursorAdapter containing the suggestions
334 * @param position The suggestion we'll be launching from
335 *
336 * @return Returns true if a successful launch, false if could not (e.g. bad position)
337 */
338 private boolean launchSuggestion(CursorAdapter ca, int position) {
339 if (ca != null) {
340 Cursor c = ca.getCursor();
341 if ((c != null) && c.moveToPosition(position)) {
342 setupSuggestionIntent(c, mSearchable);
343
344 SearchableInfo si = mSearchable;
345 String suggestionAction = mSuggestionAction;
346 Uri suggestionData = mSuggestionData;
347 String suggestionQuery = mSuggestionQuery;
348 sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, null,
349 KeyEvent.KEYCODE_UNKNOWN, null, si);
350 return true;
351 }
352 }
353 return false;
354 }
355
356 /**
357 * When a particular suggestion has been selected, perform the various lookups required
358 * to use the suggestion. This includes checking the cursor for suggestion-specific data,
359 * and/or falling back to the XML for defaults; It also creates REST style Uri data when
360 * the suggestion includes a data id.
361 *
362 * NOTE: Return values are in member variables mSuggestionAction, mSuggestionData and
363 * mSuggestionQuery.
364 *
365 * This is copied from SearchDialog.
366 *
367 * @param c The suggestions cursor, moved to the row of the user's selection
368 * @param si The searchable activity's info record
369 */
370 void setupSuggestionIntent(Cursor c, SearchableInfo si) {
371 try {
372 // use specific action if supplied, or default action if supplied, or fixed default
373 mSuggestionAction = null;
374 int column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
375 if (column >= 0) {
376 final String action = c.getString(column);
377 if (action != null) {
378 mSuggestionAction = action;
379 }
380 }
381 if (mSuggestionAction == null) {
382 mSuggestionAction = si.getSuggestIntentAction();
383 }
384 if (mSuggestionAction == null) {
385 mSuggestionAction = Intent.ACTION_SEARCH;
386 }
387
388 // use specific data if supplied, or default data if supplied
389 String data = null;
390 column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
391 if (column >= 0) {
392 final String rowData = c.getString(column);
393 if (rowData != null) {
394 data = rowData;
395 }
396 }
397 if (data == null) {
398 data = si.getSuggestIntentData();
399 }
400
401 // then, if an ID was provided, append it.
402 if (data != null) {
403 column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
404 if (column >= 0) {
405 final String id = c.getString(column);
406 if (id != null) {
407 data = data + "/" + Uri.encode(id);
408 }
409 }
410 }
411 mSuggestionData = (data == null) ? null : Uri.parse(data);
412
413 mSuggestionQuery = null;
414 column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
415 if (column >= 0) {
416 final String query = c.getString(column);
417 if (query != null) {
418 mSuggestionQuery = query;
419 }
420 }
421 } catch (RuntimeException e ) {
422 int rowNum;
423 try { // be really paranoid now
424 rowNum = c.getPosition();
425 } catch (RuntimeException e2 ) {
426 rowNum = -1;
427 }
428 Log.w(TAG, "Search Suggestions cursor at row " + rowNum +
429 " returned exception" + e.toString());
430 }
431 }
432
433 /**
434 * This class provides the filtering-based interface to suggestions providers.
435 */
436 private static class SuggestionsAdapter extends SimpleCursorAdapter {
437 public final static String[] ONE_LINE_FROM = { SearchManager.SUGGEST_COLUMN_TEXT_1 };
438 public final static int[] ONE_LINE_TO = { com.android.internal.R.id.text1 };
439
440 private final String TAG = "SuggestionsAdapter";
441
442 Filter mFilter;
443 SearchableInfo mSearchable;
444 private Resources mProviderResources;
445 String[] mFromStrings;
446
447 public SuggestionsAdapter(Context context, int layout, Cursor c,
448 String[] from, int[] to, SearchableInfo searchable) {
449 super(context, layout, c, from, to);
450 mFromStrings = from;
451 mSearchable = searchable;
452
453 // set up provider resources (gives us icons, etc.)
454 Context activityContext = mSearchable.getActivityContext(mContext);
455 Context providerContext = mSearchable.getProviderContext(mContext, activityContext);
456 mProviderResources = providerContext.getResources();
457 }
458
459 /**
460 * Use the search suggestions provider to obtain a live cursor. This will be called
461 * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
462 * The results will be processed in the UI thread and changeCursor() will be called.
463 */
464 @Override
465 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
466 String query = (constraint == null) ? "" : constraint.toString();
467 return getSuggestions(mSearchable, query);
468 }
469
470 /**
471 * Overriding this allows us to write the selected query back into the box.
472 * NOTE: This is a vastly simplified version of SearchDialog.jamQuery() and does
473 * not universally support the search API. But it is sufficient for Google Search.
474 */
475 @Override
476 public CharSequence convertToString(Cursor cursor) {
477 CharSequence result = null;
478 if (cursor != null) {
479 int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
480 if (column >= 0) {
481 final String query = cursor.getString(column);
482 if (query != null) {
483 result = query;
484 }
485 }
486 }
487 return result;
488 }
489
490 /**
491 * Get the query cursor for the search suggestions.
492 *
493 * TODO this is functionally identical to the version in SearchDialog.java. Perhaps it
494 * could be hoisted into SearchableInfo or some other shared spot.
495 *
496 * @param query The search text entered (so far)
497 * @return Returns a cursor with suggestions, or null if no suggestions
498 */
499 private Cursor getSuggestions(final SearchableInfo searchable, final String query) {
500 Cursor cursor = null;
501 if (searchable.getSuggestAuthority() != null) {
502 try {
503 StringBuilder uriStr = new StringBuilder("content://");
504 uriStr.append(searchable.getSuggestAuthority());
505
506 // if content path provided, insert it now
507 final String contentPath = searchable.getSuggestPath();
508 if (contentPath != null) {
509 uriStr.append('/');
510 uriStr.append(contentPath);
511 }
512
513 // append standard suggestion query path
514 uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY);
515
516 // inject query, either as selection args or inline
517 String[] selArgs = null;
518 if (searchable.getSuggestSelection() != null) { // use selection if provided
519 selArgs = new String[] {query};
520 } else {
521 uriStr.append('/'); // no sel, use REST pattern
522 uriStr.append(Uri.encode(query));
523 }
524
525 // finally, make the query
526 cursor = mContext.getContentResolver().query(
527 Uri.parse(uriStr.toString()), null,
528 searchable.getSuggestSelection(), selArgs,
529 null);
530 } catch (RuntimeException e) {
531 Log.w(TAG, "Search Suggestions query returned exception " + e.toString());
532 cursor = null;
533 }
534 }
535
536 return cursor;
537 }
538
539 /**
540 * Overriding this allows us to affect the way that an icon is loaded. Specifically,
541 * we can be more controlling about the resource path (and allow icons to come from other
542 * packages).
543 *
544 * TODO: This is 100% identical to the version in SearchDialog.java
545 *
546 * @param v ImageView to receive an image
547 * @param value the value retrieved from the cursor
548 */
549 @Override
550 public void setViewImage(ImageView v, String value) {
551 int resID;
552 Drawable img = null;
553
554 try {
555 resID = Integer.parseInt(value);
556 if (resID != 0) {
557 img = mProviderResources.getDrawable(resID);
558 }
559 } catch (NumberFormatException nfe) {
560 // img = null;
561 } catch (NotFoundException e2) {
562 // img = null;
563 }
564
565 // finally, set the image to whatever we've gotten
566 v.setImageDrawable(img);
567 }
568
569 /**
570 * This method is overridden purely to provide a bit of protection against
571 * flaky content providers.
572 *
573 * TODO: This is 100% identical to the version in SearchDialog.java
574 *
575 * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
576 */
577 @Override
578 public View getView(int position, View convertView, ViewGroup parent) {
579 try {
580 return super.getView(position, convertView, parent);
581 } catch (RuntimeException e) {
582 Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString());
583 // what can I return here?
584 View v = newView(mContext, mCursor, parent);
585 if (v != null) {
586 TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1);
587 tv.setText(e.toString());
588 }
589 return v;
590 }
591 }
592
593 }
594}