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