595 lines
22 KiB
Java
595 lines
22 KiB
Java
/*
|
|
* Copyright (C) 2008 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.launcher;
|
|
|
|
import android.app.ISearchManager;
|
|
import android.app.SearchManager;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.res.Resources;
|
|
import android.content.res.Resources.NotFoundException;
|
|
import android.database.Cursor;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.RemoteException;
|
|
import android.os.ServiceManager;
|
|
import android.server.search.SearchableInfo;
|
|
import android.text.Editable;
|
|
import android.text.TextUtils;
|
|
import android.text.TextWatcher;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.View.OnClickListener;
|
|
import android.view.View.OnKeyListener;
|
|
import android.view.View.OnLongClickListener;
|
|
import android.widget.AdapterView;
|
|
import android.widget.AutoCompleteTextView;
|
|
import android.widget.Button;
|
|
import android.widget.CursorAdapter;
|
|
import android.widget.Filter;
|
|
import android.widget.ImageView;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.SimpleCursorAdapter;
|
|
import android.widget.TextView;
|
|
import android.widget.AdapterView.OnItemClickListener;
|
|
import android.widget.AdapterView.OnItemSelectedListener;
|
|
|
|
public class Search extends LinearLayout implements OnClickListener, OnKeyListener,
|
|
OnLongClickListener, TextWatcher, OnItemClickListener, OnItemSelectedListener {
|
|
|
|
private final String TAG = "SearchGadget";
|
|
|
|
private AutoCompleteTextView mSearchText;
|
|
private Button mGoButton;
|
|
private OnLongClickListener mLongClickListener;
|
|
|
|
// Support for suggestions
|
|
private SuggestionsAdapter mSuggestionsAdapter;
|
|
private SearchableInfo mSearchable;
|
|
private String mSuggestionAction = null;
|
|
private Uri mSuggestionData = null;
|
|
private String mSuggestionQuery = null;
|
|
private int mItemSelected = -1;
|
|
|
|
/**
|
|
* Used to inflate the Workspace from XML.
|
|
*
|
|
* @param context The application's context.
|
|
* @param attrs The attribtues set containing the Workspace's customization values.
|
|
*/
|
|
public Search(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
}
|
|
|
|
/**
|
|
* Implements OnClickListener (for button)
|
|
*/
|
|
public void onClick(View v) {
|
|
query();
|
|
}
|
|
|
|
private void query() {
|
|
String query = mSearchText.getText().toString();
|
|
if (TextUtils.getTrimmedLength(mSearchText.getText()) == 0) {
|
|
return;
|
|
}
|
|
sendLaunchIntent(Intent.ACTION_SEARCH, null, query, null, 0, null, mSearchable);
|
|
}
|
|
|
|
/**
|
|
* Assemble a search intent and send it.
|
|
*
|
|
* This is copied from SearchDialog.
|
|
*
|
|
* @param action The intent to send, typically Intent.ACTION_SEARCH
|
|
* @param data The data for the intent
|
|
* @param query The user text entered (so far)
|
|
* @param appData The app data bundle (if supplied)
|
|
* @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will
|
|
* be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
|
|
* @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the
|
|
* corresponding tag message will be sent here. Pass null for no actionKey message.
|
|
* @param si Reference to the current SearchableInfo. Passed here so it can be used even after
|
|
* we've called dismiss(), which attempts to null mSearchable.
|
|
*/
|
|
private void sendLaunchIntent(final String action, final Uri data, final String query,
|
|
final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) {
|
|
Intent launcher = new Intent(action);
|
|
launcher.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
|
|
if (query != null) {
|
|
launcher.putExtra(SearchManager.QUERY, query);
|
|
}
|
|
|
|
if (data != null) {
|
|
launcher.setData(data);
|
|
}
|
|
|
|
if (appData != null) {
|
|
launcher.putExtra(SearchManager.APP_DATA, appData);
|
|
}
|
|
|
|
// add launch info (action key, etc.)
|
|
if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
|
|
launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
|
|
launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
|
|
}
|
|
|
|
// attempt to enforce security requirement (no 3rd-party intents)
|
|
if (si != null) {
|
|
launcher.setComponent(si.mSearchActivity);
|
|
}
|
|
|
|
getContext().startActivity(launcher);
|
|
}
|
|
|
|
/**
|
|
* Implements TextWatcher (for EditText)
|
|
*/
|
|
public void beforeTextChanged(CharSequence s, int start, int before, int after) {
|
|
}
|
|
|
|
/**
|
|
* Implements TextWatcher (for EditText)
|
|
*/
|
|
public void onTextChanged(CharSequence s, int start, int before, int after) {
|
|
// enable the button if we have one or more non-space characters
|
|
boolean enabled = TextUtils.getTrimmedLength(mSearchText.getText()) != 0;
|
|
mGoButton.setEnabled(enabled);
|
|
mGoButton.setFocusable(enabled);
|
|
}
|
|
|
|
/**
|
|
* Implements TextWatcher (for EditText)
|
|
*/
|
|
public void afterTextChanged(Editable s) {
|
|
}
|
|
|
|
/**
|
|
* Implements OnKeyListener (for EditText and for button)
|
|
*
|
|
* This plays some games with state in order to "soften" the strength of suggestions
|
|
* presented. Suggestions should not be used unless the user specifically navigates to them
|
|
* (or clicks them, in which case it's obvious). This is not the way that AutoCompleteTextBox
|
|
* normally works.
|
|
*/
|
|
public final boolean onKey(View v, int keyCode, KeyEvent event) {
|
|
if (v == mSearchText) {
|
|
boolean searchTrigger = (keyCode == KeyEvent.KEYCODE_ENTER ||
|
|
keyCode == KeyEvent.KEYCODE_SEARCH ||
|
|
keyCode == KeyEvent.KEYCODE_DPAD_CENTER);
|
|
if (event.getAction() == KeyEvent.ACTION_UP) {
|
|
// Log.d(TAG, "onKey() ACTION_UP isPopupShowing:" + mSearchText.isPopupShowing());
|
|
if (!mSearchText.isPopupShowing()) {
|
|
if (searchTrigger) {
|
|
query();
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
// Log.d(TAG, "onKey() ACTION_DOWN isPopupShowing:" + mSearchText.isPopupShowing() +
|
|
// " mItemSelected="+ mItemSelected);
|
|
if (searchTrigger && mItemSelected < 0) {
|
|
query();
|
|
return true;
|
|
}
|
|
}
|
|
} else if (v == mGoButton) {
|
|
boolean handled = false;
|
|
if (!event.isSystem() &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
|
|
if (mSearchText.requestFocus()) {
|
|
handled = mSearchText.dispatchKeyEvent(event);
|
|
}
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void setOnLongClickListener(OnLongClickListener l) {
|
|
super.setOnLongClickListener(l);
|
|
mLongClickListener = l;
|
|
}
|
|
|
|
/**
|
|
* Implements OnLongClickListener (for button)
|
|
*/
|
|
public boolean onLongClick(View v) {
|
|
// Pretend that a long press on a child view is a long press on the search widget
|
|
if (mLongClickListener != null) {
|
|
return mLongClickListener.onLongClick(this);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
requestFocusFromTouch();
|
|
return super.onInterceptTouchEvent(ev);
|
|
}
|
|
|
|
/**
|
|
* In order to keep things simple, the external trigger will clear the query just before
|
|
* focusing, so as to give you a fresh query. This way we eliminate any sources of
|
|
* accidental query launching.
|
|
*/
|
|
public void clearQuery() {
|
|
mSearchText.setText(null);
|
|
}
|
|
|
|
@Override
|
|
protected void onFinishInflate() {
|
|
super.onFinishInflate();
|
|
|
|
mSearchText = (AutoCompleteTextView) findViewById(R.id.input);
|
|
// TODO: This can be confusing when the user taps the text field to give the focus
|
|
// (it is not necessary but I ran into this issue several times myself)
|
|
// mTitleInput.setOnClickListener(this);
|
|
mSearchText.setOnKeyListener(this);
|
|
mSearchText.addTextChangedListener(this);
|
|
|
|
mGoButton = (Button) findViewById(R.id.go);
|
|
mGoButton.setOnClickListener(this);
|
|
mGoButton.setOnKeyListener(this);
|
|
|
|
mSearchText.setOnLongClickListener(this);
|
|
mGoButton.setOnLongClickListener(this);
|
|
|
|
// disable the button since we start out w/empty input
|
|
mGoButton.setEnabled(false);
|
|
mGoButton.setFocusable(false);
|
|
|
|
configureSuggestions();
|
|
}
|
|
|
|
/** The rest of the class deals with providing search suggestions */
|
|
|
|
/**
|
|
* Set up the suggestions provider mechanism
|
|
*/
|
|
private void configureSuggestions() {
|
|
// get SearchableInfo
|
|
ISearchManager sms;
|
|
SearchableInfo searchable;
|
|
sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE));
|
|
try {
|
|
// TODO null isn't the published use of this API, but it works when global=true
|
|
// TODO better implementation: defer all of this, let Home set it up
|
|
searchable = sms.getSearchableInfo(null, true);
|
|
} catch (RemoteException e) {
|
|
searchable = null;
|
|
}
|
|
if (searchable == null) {
|
|
// no suggestions so just get out (no need to continue)
|
|
return;
|
|
}
|
|
mSearchable = searchable;
|
|
|
|
mSearchText.setOnItemClickListener(this);
|
|
mSearchText.setOnItemSelectedListener(this);
|
|
|
|
// attach the suggestions adapter
|
|
mSuggestionsAdapter = new SuggestionsAdapter(mContext,
|
|
com.android.internal.R.layout.search_dropdown_item_1line, null,
|
|
SuggestionsAdapter.ONE_LINE_FROM, SuggestionsAdapter.ONE_LINE_TO, mSearchable);
|
|
mSearchText.setAdapter(mSuggestionsAdapter);
|
|
}
|
|
|
|
/**
|
|
* Implements OnItemClickListener
|
|
*/
|
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
|
// Log.d(TAG, "onItemClick() position " + position);
|
|
launchSuggestion(mSuggestionsAdapter, position);
|
|
}
|
|
|
|
/**
|
|
* Implements OnItemSelectedListener
|
|
*/
|
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
|
// Log.d(TAG, "onItemSelected() position " + position);
|
|
mItemSelected = position;
|
|
}
|
|
|
|
/**
|
|
* Implements OnItemSelectedListener
|
|
*/
|
|
public void onNothingSelected(AdapterView<?> parent) {
|
|
// Log.d(TAG, "onNothingSelected()");
|
|
mItemSelected = -1;
|
|
}
|
|
|
|
/**
|
|
* Code to launch a suggestion query.
|
|
*
|
|
* This is copied from SearchDialog.
|
|
*
|
|
* @param ca The CursorAdapter containing the suggestions
|
|
* @param position The suggestion we'll be launching from
|
|
*
|
|
* @return Returns true if a successful launch, false if could not (e.g. bad position)
|
|
*/
|
|
private boolean launchSuggestion(CursorAdapter ca, int position) {
|
|
if (ca != null) {
|
|
Cursor c = ca.getCursor();
|
|
if ((c != null) && c.moveToPosition(position)) {
|
|
setupSuggestionIntent(c, mSearchable);
|
|
|
|
SearchableInfo si = mSearchable;
|
|
String suggestionAction = mSuggestionAction;
|
|
Uri suggestionData = mSuggestionData;
|
|
String suggestionQuery = mSuggestionQuery;
|
|
sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, null,
|
|
KeyEvent.KEYCODE_UNKNOWN, null, si);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* When a particular suggestion has been selected, perform the various lookups required
|
|
* to use the suggestion. This includes checking the cursor for suggestion-specific data,
|
|
* and/or falling back to the XML for defaults; It also creates REST style Uri data when
|
|
* the suggestion includes a data id.
|
|
*
|
|
* NOTE: Return values are in member variables mSuggestionAction, mSuggestionData and
|
|
* mSuggestionQuery.
|
|
*
|
|
* This is copied from SearchDialog.
|
|
*
|
|
* @param c The suggestions cursor, moved to the row of the user's selection
|
|
* @param si The searchable activity's info record
|
|
*/
|
|
void setupSuggestionIntent(Cursor c, SearchableInfo si) {
|
|
try {
|
|
// use specific action if supplied, or default action if supplied, or fixed default
|
|
mSuggestionAction = null;
|
|
int column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
|
|
if (column >= 0) {
|
|
final String action = c.getString(column);
|
|
if (action != null) {
|
|
mSuggestionAction = action;
|
|
}
|
|
}
|
|
if (mSuggestionAction == null) {
|
|
mSuggestionAction = si.getSuggestIntentAction();
|
|
}
|
|
if (mSuggestionAction == null) {
|
|
mSuggestionAction = Intent.ACTION_SEARCH;
|
|
}
|
|
|
|
// use specific data if supplied, or default data if supplied
|
|
String data = null;
|
|
column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
|
|
if (column >= 0) {
|
|
final String rowData = c.getString(column);
|
|
if (rowData != null) {
|
|
data = rowData;
|
|
}
|
|
}
|
|
if (data == null) {
|
|
data = si.getSuggestIntentData();
|
|
}
|
|
|
|
// then, if an ID was provided, append it.
|
|
if (data != null) {
|
|
column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
|
|
if (column >= 0) {
|
|
final String id = c.getString(column);
|
|
if (id != null) {
|
|
data = data + "/" + Uri.encode(id);
|
|
}
|
|
}
|
|
}
|
|
mSuggestionData = (data == null) ? null : Uri.parse(data);
|
|
|
|
mSuggestionQuery = null;
|
|
column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
|
|
if (column >= 0) {
|
|
final String query = c.getString(column);
|
|
if (query != null) {
|
|
mSuggestionQuery = query;
|
|
}
|
|
}
|
|
} catch (RuntimeException e ) {
|
|
int rowNum;
|
|
try { // be really paranoid now
|
|
rowNum = c.getPosition();
|
|
} catch (RuntimeException e2 ) {
|
|
rowNum = -1;
|
|
}
|
|
Log.w(TAG, "Search Suggestions cursor at row " + rowNum +
|
|
" returned exception" + e.toString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This class provides the filtering-based interface to suggestions providers.
|
|
*/
|
|
private static class SuggestionsAdapter extends SimpleCursorAdapter {
|
|
public final static String[] ONE_LINE_FROM = { SearchManager.SUGGEST_COLUMN_TEXT_1 };
|
|
public final static int[] ONE_LINE_TO = { com.android.internal.R.id.text1 };
|
|
|
|
private final String TAG = "SuggestionsAdapter";
|
|
|
|
Filter mFilter;
|
|
SearchableInfo mSearchable;
|
|
private Resources mProviderResources;
|
|
String[] mFromStrings;
|
|
|
|
public SuggestionsAdapter(Context context, int layout, Cursor c,
|
|
String[] from, int[] to, SearchableInfo searchable) {
|
|
super(context, layout, c, from, to);
|
|
mFromStrings = from;
|
|
mSearchable = searchable;
|
|
|
|
// set up provider resources (gives us icons, etc.)
|
|
Context activityContext = mSearchable.getActivityContext(mContext);
|
|
Context providerContext = mSearchable.getProviderContext(mContext, activityContext);
|
|
mProviderResources = providerContext.getResources();
|
|
}
|
|
|
|
/**
|
|
* Use the search suggestions provider to obtain a live cursor. This will be called
|
|
* in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
|
|
* The results will be processed in the UI thread and changeCursor() will be called.
|
|
*/
|
|
@Override
|
|
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
|
|
String query = (constraint == null) ? "" : constraint.toString();
|
|
return getSuggestions(mSearchable, query);
|
|
}
|
|
|
|
/**
|
|
* Overriding this allows us to write the selected query back into the box.
|
|
* NOTE: This is a vastly simplified version of SearchDialog.jamQuery() and does
|
|
* not universally support the search API. But it is sufficient for Google Search.
|
|
*/
|
|
@Override
|
|
public CharSequence convertToString(Cursor cursor) {
|
|
CharSequence result = null;
|
|
if (cursor != null) {
|
|
int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
|
|
if (column >= 0) {
|
|
final String query = cursor.getString(column);
|
|
if (query != null) {
|
|
result = query;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get the query cursor for the search suggestions.
|
|
*
|
|
* TODO this is functionally identical to the version in SearchDialog.java. Perhaps it
|
|
* could be hoisted into SearchableInfo or some other shared spot.
|
|
*
|
|
* @param query The search text entered (so far)
|
|
* @return Returns a cursor with suggestions, or null if no suggestions
|
|
*/
|
|
private Cursor getSuggestions(final SearchableInfo searchable, final String query) {
|
|
Cursor cursor = null;
|
|
if (searchable.getSuggestAuthority() != null) {
|
|
try {
|
|
StringBuilder uriStr = new StringBuilder("content://");
|
|
uriStr.append(searchable.getSuggestAuthority());
|
|
|
|
// if content path provided, insert it now
|
|
final String contentPath = searchable.getSuggestPath();
|
|
if (contentPath != null) {
|
|
uriStr.append('/');
|
|
uriStr.append(contentPath);
|
|
}
|
|
|
|
// append standard suggestion query path
|
|
uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY);
|
|
|
|
// inject query, either as selection args or inline
|
|
String[] selArgs = null;
|
|
if (searchable.getSuggestSelection() != null) { // use selection if provided
|
|
selArgs = new String[] {query};
|
|
} else {
|
|
uriStr.append('/'); // no sel, use REST pattern
|
|
uriStr.append(Uri.encode(query));
|
|
}
|
|
|
|
// finally, make the query
|
|
cursor = mContext.getContentResolver().query(
|
|
Uri.parse(uriStr.toString()), null,
|
|
searchable.getSuggestSelection(), selArgs,
|
|
null);
|
|
} catch (RuntimeException e) {
|
|
Log.w(TAG, "Search Suggestions query returned exception " + e.toString());
|
|
cursor = null;
|
|
}
|
|
}
|
|
|
|
return cursor;
|
|
}
|
|
|
|
/**
|
|
* Overriding this allows us to affect the way that an icon is loaded. Specifically,
|
|
* we can be more controlling about the resource path (and allow icons to come from other
|
|
* packages).
|
|
*
|
|
* TODO: This is 100% identical to the version in SearchDialog.java
|
|
*
|
|
* @param v ImageView to receive an image
|
|
* @param value the value retrieved from the cursor
|
|
*/
|
|
@Override
|
|
public void setViewImage(ImageView v, String value) {
|
|
int resID;
|
|
Drawable img = null;
|
|
|
|
try {
|
|
resID = Integer.parseInt(value);
|
|
if (resID != 0) {
|
|
img = mProviderResources.getDrawable(resID);
|
|
}
|
|
} catch (NumberFormatException nfe) {
|
|
// img = null;
|
|
} catch (NotFoundException e2) {
|
|
// img = null;
|
|
}
|
|
|
|
// finally, set the image to whatever we've gotten
|
|
v.setImageDrawable(img);
|
|
}
|
|
|
|
/**
|
|
* This method is overridden purely to provide a bit of protection against
|
|
* flaky content providers.
|
|
*
|
|
* TODO: This is 100% identical to the version in SearchDialog.java
|
|
*
|
|
* @see android.widget.ListAdapter#getView(int, View, ViewGroup)
|
|
*/
|
|
@Override
|
|
public View getView(int position, View convertView, ViewGroup parent) {
|
|
try {
|
|
return super.getView(position, convertView, parent);
|
|
} catch (RuntimeException e) {
|
|
Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString());
|
|
// what can I return here?
|
|
View v = newView(mContext, mCursor, parent);
|
|
if (v != null) {
|
|
TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1);
|
|
tv.setText(e.toString());
|
|
}
|
|
return v;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|