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