Merge "SearchView API"
This commit is contained in:
committed by
Android (Google) Code Review
commit
971e1c64e2
645
core/java/android/widget/SearchView.java
Normal file
645
core/java/android/widget/SearchView.java
Normal file
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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 android.widget;
|
||||
|
||||
import static android.widget.SuggestionsAdapter.getColumnString;
|
||||
|
||||
import com.android.internal.R;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.app.SearchableInfo;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
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.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Provides the user interface elements for the user to enter a search query and submit a
|
||||
* request to a search provider. Shows a list of query suggestions or results, if
|
||||
* available and allows the user to pick a suggestion or result to launch into.
|
||||
*/
|
||||
public class SearchView extends LinearLayout {
|
||||
|
||||
private static final boolean DBG = false;
|
||||
private static final String LOG_TAG = "SearchView";
|
||||
|
||||
private OnQueryChangeListener mOnQueryChangeListener;
|
||||
private OnCloseListener mOnCloseListener;
|
||||
|
||||
private boolean mIconifiedByDefault;
|
||||
private CursorAdapter mSuggestionsAdapter;
|
||||
private View mSearchButton;
|
||||
private View mSubmitButton;
|
||||
private View mCloseButton;
|
||||
private View mSearchEditFrame;
|
||||
private AutoCompleteTextView mQueryTextView;
|
||||
private boolean mSubmitButtonEnabled;
|
||||
private CharSequence mQueryHint;
|
||||
|
||||
private SearchableInfo mSearchable;
|
||||
|
||||
// A weak map of drawables we've gotten from other packages, so we don't load them
|
||||
// more than once.
|
||||
private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
|
||||
new WeakHashMap<String, Drawable.ConstantState>();
|
||||
|
||||
/**
|
||||
* Callbacks for changes to the query text.
|
||||
*/
|
||||
public interface OnQueryChangeListener {
|
||||
|
||||
/**
|
||||
* Called when the user submits the query. This could be due to a key press on the
|
||||
* keyboard or due to pressing a submit button.
|
||||
* The listener can override the standard behavior by returning true
|
||||
* to indicate that it has handled the submit request. Otherwise return false to
|
||||
* let the SearchView handle the submission by launching any associated intent.
|
||||
*
|
||||
* @param query the query text that is to be submitted
|
||||
*
|
||||
* @return true if the query has been handled by the listener, false to let the
|
||||
* SearchView perform the default action.
|
||||
*/
|
||||
boolean onSubmitQuery(String query);
|
||||
|
||||
/**
|
||||
* Called when the query text is changed by the user.
|
||||
*
|
||||
* @param newText the new content of the query text field.
|
||||
*
|
||||
* @return false if the SearchView should perform the default action of showing any
|
||||
* suggestions if available, true if the action was handled by the listener.
|
||||
*/
|
||||
boolean onQueryTextChanged(String newText);
|
||||
}
|
||||
|
||||
public interface OnCloseListener {
|
||||
|
||||
/**
|
||||
* The user is attempting to close the SearchView.
|
||||
*
|
||||
* @return true if the listener wants to override the default behavior of clearing the
|
||||
* text field and dismissing it, false otherwise.
|
||||
*/
|
||||
boolean onClose();
|
||||
}
|
||||
|
||||
public SearchView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SearchView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.search_view, this, true);
|
||||
|
||||
mSearchButton = findViewById(R.id.search_button);
|
||||
mQueryTextView = (AutoCompleteTextView) findViewById(R.id.search_src_text);
|
||||
mSearchEditFrame = findViewById(R.id.search_edit_frame);
|
||||
mSubmitButton = findViewById(R.id.search_go_btn);
|
||||
mCloseButton = findViewById(R.id.search_close_btn);
|
||||
|
||||
mSearchButton.setOnClickListener(mOnClickListener);
|
||||
mCloseButton.setOnClickListener(mOnClickListener);
|
||||
mSubmitButton.setOnClickListener(mOnClickListener);
|
||||
mQueryTextView.addTextChangedListener(mTextWatcher);
|
||||
mQueryTextView.setOnEditorActionListener(mOnEditorActionListener);
|
||||
mQueryTextView.setOnItemClickListener(mOnItemClickListener);
|
||||
mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener);
|
||||
|
||||
mSubmitButtonEnabled = false;
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SearchView, 0, 0);
|
||||
setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
|
||||
a.recycle();
|
||||
|
||||
updateViewsVisibility(mIconifiedByDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
|
||||
* to display labels, hints, suggestions, create intents for launching search results screens
|
||||
* and controlling other affordances such as a voice button.
|
||||
*
|
||||
* @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
|
||||
* activity or a global search provider.
|
||||
*/
|
||||
public void setSearchableInfo(SearchableInfo searchable) {
|
||||
mSearchable = searchable;
|
||||
if (mSearchable != null) {
|
||||
updateSearchAutoComplete();
|
||||
}
|
||||
updateViewsVisibility(mIconifiedByDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a listener for user actions within the SearchView.
|
||||
*
|
||||
* @param listener the listener object that receives callbacks when the user performs
|
||||
* actions in the SearchView such as clicking on buttons or typing a query.
|
||||
*/
|
||||
public void setOnQueryChangeListener(OnQueryChangeListener listener) {
|
||||
mOnQueryChangeListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a query string in the text field and optionally submits the query as well.
|
||||
*
|
||||
* @param query the query string. This replaces any query text already present in the
|
||||
* text field.
|
||||
* @param submit whether to submit the query right now or only update the contents of
|
||||
* text field.
|
||||
*/
|
||||
public void setQuery(CharSequence query, boolean submit) {
|
||||
mQueryTextView.setText(query);
|
||||
// If the query is not empty and submit is requested, submit the query
|
||||
if (submit && !TextUtils.isEmpty(query)) {
|
||||
onSubmitQuery();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hint text to display in the query text field. This overrides any hint specified
|
||||
* in the SearchableInfo.
|
||||
*
|
||||
* @param hint the hint text to display
|
||||
*/
|
||||
public void setQueryHint(CharSequence hint) {
|
||||
mQueryHint = hint;
|
||||
updateQueryHint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default or resting state of the search field. If true, a single search icon is
|
||||
* shown by default and expands to show the text field and other buttons when pressed. Also,
|
||||
* if the default state is iconified, then it collapses to that state when the close button
|
||||
* is pressed.
|
||||
*
|
||||
* @param iconified
|
||||
*/
|
||||
public void setIconifiedByDefault(boolean iconified) {
|
||||
mIconifiedByDefault = iconified;
|
||||
updateViewsVisibility(iconified);
|
||||
}
|
||||
|
||||
public boolean isIconfiedByDefault() {
|
||||
return mIconifiedByDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables showing a submit button when the query is non-empty. In cases where the SearchView
|
||||
* is being used to filter the contents of the current activity and doesn't launch a separate
|
||||
* results activity, then the submit button should be disabled.
|
||||
*
|
||||
* @param enabled true to show a submit button for submitting queries, false if a submit
|
||||
* button is not required.
|
||||
*/
|
||||
public void setSubmitButtonEnabled(boolean enabled) {
|
||||
mSubmitButton.setVisibility(enabled ? VISIBLE : GONE);
|
||||
mSubmitButtonEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the submit button is enabled when necessary or never displayed.
|
||||
*
|
||||
* @return whether the submit button is enabled automatically when necessary
|
||||
*/
|
||||
public boolean isSubmitButtonEnabled() {
|
||||
return mSubmitButtonEnabled;
|
||||
}
|
||||
|
||||
public interface FilterableListAdapter extends ListAdapter, Filterable {
|
||||
}
|
||||
|
||||
/**
|
||||
* You can set a custom adapter if you wish. Otherwise the default adapter is used to
|
||||
* display the suggestions from the suggestions provider associated with the SearchableInfo.
|
||||
*
|
||||
* @see #setSearchableInfo(SearchableInfo)
|
||||
*/
|
||||
public void setSuggestionsAdapter(CursorAdapter adapter) {
|
||||
mSuggestionsAdapter = adapter;
|
||||
|
||||
mQueryTextView.setAdapter(mSuggestionsAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the adapter used for suggestions, if any.
|
||||
* @return the suggestions adapter
|
||||
*/
|
||||
public CursorAdapter getSuggestionsAdapter() {
|
||||
return mSuggestionsAdapter;
|
||||
}
|
||||
|
||||
private void updateViewsVisibility(boolean collapsed) {
|
||||
// Visibility of views that are visible when collapsed
|
||||
int visCollapsed = collapsed? VISIBLE : GONE;
|
||||
// Visibility of views that are visible when expanded
|
||||
int visExpanded = collapsed? GONE : VISIBLE;
|
||||
|
||||
mSearchButton.setVisibility(visCollapsed);
|
||||
mSubmitButton.setVisibility(mSubmitButtonEnabled ? visExpanded : GONE);
|
||||
mSearchEditFrame.setVisibility(visExpanded);
|
||||
|
||||
setImeVisibility(!collapsed);
|
||||
}
|
||||
|
||||
private void setImeVisibility(boolean visible) {
|
||||
// We made sure the IME was displayed, so also make sure it is closed
|
||||
// when we go away.
|
||||
InputMethodManager imm = (InputMethodManager)
|
||||
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
if (visible) {
|
||||
imm.showSoftInputUnchecked(0, null);
|
||||
} else {
|
||||
imm.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final OnClickListener mOnClickListener = new OnClickListener() {
|
||||
|
||||
public void onClick(View v) {
|
||||
if (v == mSearchButton) {
|
||||
onSearchClicked();
|
||||
} else if (v == mCloseButton) {
|
||||
onCloseClicked();
|
||||
} else if (v == mSubmitButton) {
|
||||
onSubmitQuery();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the key down event for dealing with action keys.
|
||||
*
|
||||
* @param keyCode This is the keycode of the typed key, and is the same value as
|
||||
* found in the KeyEvent parameter.
|
||||
* @param event The complete event record for the typed key
|
||||
*
|
||||
* @return true if the event was handled here, or false if not.
|
||||
*/
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (mSearchable == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if it's an action specified by the searchable activity, launch the
|
||||
// entered query with the action key
|
||||
SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
|
||||
if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
|
||||
launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView.getText().toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
private void updateQueryHint() {
|
||||
if (mQueryHint != null) {
|
||||
mQueryTextView.setHint(mQueryHint);
|
||||
} else if (mSearchable != null) {
|
||||
CharSequence hint = null;
|
||||
int hintId = mSearchable.getHintId();
|
||||
if (hintId != 0) {
|
||||
hint = getContext().getString(hintId);
|
||||
}
|
||||
if (hint != null) {
|
||||
mQueryTextView.setHint(hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the auto-complete text view.
|
||||
*/
|
||||
private void updateSearchAutoComplete() {
|
||||
// close any existing suggestions adapter
|
||||
//closeSuggestionsAdapter();
|
||||
|
||||
mQueryTextView.setDropDownAnimationStyle(0); // no animation
|
||||
|
||||
// attach the suggestions adapter, if suggestions are available
|
||||
// The existence of a suggestions authority is the proxy for "suggestions available here"
|
||||
if (mSearchable.getSuggestAuthority() != null) {
|
||||
mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
|
||||
this, mSearchable, mOutsideDrawablesCache);
|
||||
mQueryTextView.setAdapter(mSuggestionsAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
|
||||
|
||||
/**
|
||||
* Called when the input method default action key is pressed.
|
||||
*/
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
onSubmitQuery();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private void onTextChanged(CharSequence newText) {
|
||||
CharSequence text = mQueryTextView.getText();
|
||||
boolean hasText = !TextUtils.isEmpty(text);
|
||||
if (isSubmitButtonEnabled()) {
|
||||
mSubmitButton.setVisibility(hasText ? VISIBLE : GONE);
|
||||
}
|
||||
if (mOnQueryChangeListener != null)
|
||||
mOnQueryChangeListener.onQueryTextChanged(newText.toString());
|
||||
}
|
||||
|
||||
private void onSubmitQuery() {
|
||||
CharSequence query = mQueryTextView.getText();
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
if (mOnQueryChangeListener == null
|
||||
|| !mOnQueryChangeListener.onSubmitQuery(query.toString())) {
|
||||
if (mSearchable != null) {
|
||||
launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onCloseClicked() {
|
||||
if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
|
||||
mQueryTextView.setText("");
|
||||
updateViewsVisibility(mIconifiedByDefault);
|
||||
}
|
||||
}
|
||||
|
||||
private void onSearchClicked() {
|
||||
mQueryTextView.requestFocus();
|
||||
updateViewsVisibility(false);
|
||||
}
|
||||
|
||||
private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
|
||||
|
||||
/**
|
||||
* Implements OnItemClickListener
|
||||
*/
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DBG)
|
||||
Log.d(LOG_TAG, "onItemClick() position " + position);
|
||||
launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
|
||||
}
|
||||
};
|
||||
|
||||
private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
|
||||
|
||||
/**
|
||||
* Implements OnItemSelectedListener
|
||||
*/
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DBG)
|
||||
Log.d(LOG_TAG, "onItemSelected() position " + position);
|
||||
// A suggestion has been selected, rewrite the query if possible,
|
||||
// otherwise the restore the original query.
|
||||
rewriteQueryFromSuggestion(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements OnItemSelectedListener
|
||||
*/
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
if (DBG)
|
||||
Log.d(LOG_TAG, "onNothingSelected()");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Query rewriting.
|
||||
*/
|
||||
private void rewriteQueryFromSuggestion(int position) {
|
||||
CharSequence oldQuery = mQueryTextView.getText();
|
||||
Cursor c = mSuggestionsAdapter.getCursor();
|
||||
if (c == null) {
|
||||
return;
|
||||
}
|
||||
if (c.moveToPosition(position)) {
|
||||
// Get the new query from the suggestion.
|
||||
CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
|
||||
if (newQuery != null) {
|
||||
// The suggestion rewrites the query.
|
||||
// Update the text field, without getting new suggestions.
|
||||
setQuery(newQuery);
|
||||
} else {
|
||||
// The suggestion does not rewrite the query, restore the user's query.
|
||||
setQuery(oldQuery);
|
||||
}
|
||||
} else {
|
||||
// We got a bad position, restore the user's query.
|
||||
setQuery(oldQuery);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches an intent based on a suggestion.
|
||||
*
|
||||
* @param position The index of the suggestion to create the intent from.
|
||||
* @param actionKey The key code of the action key that was pressed,
|
||||
* or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
|
||||
* @param actionMsg The message for the action key that was pressed,
|
||||
* or <code>null</code> if none.
|
||||
* @return true if a successful launch, false if could not (e.g. bad position).
|
||||
*/
|
||||
private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
|
||||
Cursor c = mSuggestionsAdapter.getCursor();
|
||||
if ((c != null) && c.moveToPosition(position)) {
|
||||
|
||||
Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
|
||||
|
||||
// launch the intent
|
||||
launchIntent(intent);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches an intent, including any special intent handling.
|
||||
*/
|
||||
private void launchIntent(Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// If the intent was created from a suggestion, it will always have an explicit
|
||||
// component here.
|
||||
getContext().startActivity(intent);
|
||||
} catch (RuntimeException ex) {
|
||||
Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text in the query box, without updating the suggestions.
|
||||
*/
|
||||
private void setQuery(CharSequence query) {
|
||||
mQueryTextView.setText(query, false);
|
||||
}
|
||||
|
||||
private void launchQuerySearch(int actionKey, String actionMsg, String query) {
|
||||
String action = Intent.ACTION_SEARCH;
|
||||
Intent intent = createIntent(action, null, null, query, null, actionKey, actionMsg);
|
||||
getContext().startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an intent from the given information and the search dialog state.
|
||||
*
|
||||
* @param action Intent action.
|
||||
* @param data Intent data, or <code>null</code>.
|
||||
* @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
|
||||
* @param query Intent query, or <code>null</code>.
|
||||
* @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>.
|
||||
* @param actionKey The key code of the action key that was pressed,
|
||||
* or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
|
||||
* @param actionMsg The message for the action key that was pressed,
|
||||
* or <code>null</code> if none.
|
||||
* @param mode The search mode, one of the acceptable values for
|
||||
* {@link SearchManager#SEARCH_MODE}, or {@code null}.
|
||||
* @return The intent.
|
||||
*/
|
||||
private Intent createIntent(String action, Uri data, String extraData, String query,
|
||||
String componentName, int actionKey, String actionMsg) {
|
||||
// Now build the Intent
|
||||
Intent intent = new Intent(action);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
// We need CLEAR_TOP to avoid reusing an old task that has other activities
|
||||
// on top of the one we want. We don't want to do this in in-app search though,
|
||||
// as it can be destructive to the activity stack.
|
||||
if (data != null) {
|
||||
intent.setData(data);
|
||||
}
|
||||
intent.putExtra(SearchManager.USER_QUERY, query);
|
||||
if (query != null) {
|
||||
intent.putExtra(SearchManager.QUERY, query);
|
||||
}
|
||||
if (extraData != null) {
|
||||
intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
|
||||
}
|
||||
if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
|
||||
intent.putExtra(SearchManager.ACTION_KEY, actionKey);
|
||||
intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
|
||||
}
|
||||
intent.setComponent(mSearchable.getSearchActivity());
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param c The suggestions cursor, moved to the row of the user's selection
|
||||
* @param actionKey The key code of the action key that was pressed,
|
||||
* or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
|
||||
* @param actionMsg The message for the action key that was pressed,
|
||||
* or <code>null</code> if none.
|
||||
* @return An intent for the suggestion at the cursor's position.
|
||||
*/
|
||||
private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
|
||||
try {
|
||||
// use specific action if supplied, or default action if supplied, or fixed default
|
||||
String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
|
||||
|
||||
// some items are display only, or have effect via the cursor respond click reporting.
|
||||
if (SearchManager.INTENT_ACTION_NONE.equals(action)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action == null) {
|
||||
action = mSearchable.getSuggestIntentAction();
|
||||
}
|
||||
if (action == null) {
|
||||
action = Intent.ACTION_SEARCH;
|
||||
}
|
||||
|
||||
// use specific data if supplied, or default data if supplied
|
||||
String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
|
||||
if (data == null) {
|
||||
data = mSearchable.getSuggestIntentData();
|
||||
}
|
||||
// then, if an ID was provided, append it.
|
||||
if (data != null) {
|
||||
String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
|
||||
if (id != null) {
|
||||
data = data + "/" + Uri.encode(id);
|
||||
}
|
||||
}
|
||||
Uri dataUri = (data == null) ? null : Uri.parse(data);
|
||||
|
||||
String componentName = getColumnString(
|
||||
c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
|
||||
|
||||
String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
|
||||
String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
|
||||
|
||||
return createIntent(action, dataUri, extraData, query, componentName, actionKey,
|
||||
actionMsg);
|
||||
} catch (RuntimeException e ) {
|
||||
int rowNum;
|
||||
try { // be really paranoid now
|
||||
rowNum = c.getPosition();
|
||||
} catch (RuntimeException e2 ) {
|
||||
rowNum = -1;
|
||||
}
|
||||
Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
|
||||
" returned exception" + e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to watch the text field for empty/non-empty
|
||||
*/
|
||||
private TextWatcher mTextWatcher = new TextWatcher() {
|
||||
|
||||
public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
|
||||
|
||||
public void onTextChanged(CharSequence s, int start,
|
||||
int before, int after) {
|
||||
SearchView.this.onTextChanged(s);
|
||||
}
|
||||
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
};
|
||||
}
|
||||
705
core/java/android/widget/SuggestionsAdapter.java
Normal file
705
core/java/android/widget/SuggestionsAdapter.java
Normal file
@@ -0,0 +1,705 @@
|
||||
/*
|
||||
* Copyright (C) 2009 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 android.widget;
|
||||
|
||||
import com.android.internal.R;
|
||||
|
||||
import android.app.SearchDialog;
|
||||
import android.app.SearchManager;
|
||||
import android.app.SearchableInfo;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver.OpenResourceIdResult;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.StateListDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.TextAppearanceSpan;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Provides the contents for the suggestion drop-down list.in {@link SearchDialog}.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
class SuggestionsAdapter extends ResourceCursorAdapter {
|
||||
|
||||
private static final boolean DBG = false;
|
||||
private static final String LOG_TAG = "SuggestionsAdapter";
|
||||
private static final int QUERY_LIMIT = 50;
|
||||
|
||||
private SearchManager mSearchManager;
|
||||
private SearchView mSearchView;
|
||||
private SearchableInfo mSearchable;
|
||||
private Context mProviderContext;
|
||||
private WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache;
|
||||
private SparseArray<Drawable.ConstantState> mBackgroundsCache;
|
||||
private boolean mClosed = false;
|
||||
|
||||
// URL color
|
||||
private ColorStateList mUrlColor;
|
||||
|
||||
// Cached column indexes, updated when the cursor changes.
|
||||
private int mText1Col;
|
||||
private int mText2Col;
|
||||
private int mText2UrlCol;
|
||||
private int mIconName1Col;
|
||||
private int mIconName2Col;
|
||||
private int mBackgroundColorCol;
|
||||
|
||||
static final int NONE = -1;
|
||||
|
||||
private final Runnable mStartSpinnerRunnable;
|
||||
private final Runnable mStopSpinnerRunnable;
|
||||
|
||||
/**
|
||||
* The amount of time we delay in the filter when the user presses the delete key.
|
||||
* @see Filter#setDelayer(android.widget.Filter.Delayer).
|
||||
*/
|
||||
private static final long DELETE_KEY_POST_DELAY = 500L;
|
||||
|
||||
public SuggestionsAdapter(Context context, SearchView searchView,
|
||||
SearchableInfo searchable,
|
||||
WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) {
|
||||
super(context,
|
||||
com.android.internal.R.layout.search_dropdown_item_icons_2line,
|
||||
null, // no initial cursor
|
||||
true); // auto-requery
|
||||
mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
|
||||
mSearchView = searchView;
|
||||
mSearchable = searchable;
|
||||
|
||||
// set up provider resources (gives us icons, etc.)
|
||||
Context activityContext = mSearchable.getActivityContext(mContext);
|
||||
mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
|
||||
|
||||
mOutsideDrawablesCache = outsideDrawablesCache;
|
||||
mBackgroundsCache = new SparseArray<Drawable.ConstantState>();
|
||||
|
||||
mStartSpinnerRunnable = new Runnable() {
|
||||
public void run() {
|
||||
// mSearchView.setWorking(true); // TODO:
|
||||
}
|
||||
};
|
||||
|
||||
mStopSpinnerRunnable = new Runnable() {
|
||||
public void run() {
|
||||
// mSearchView.setWorking(false); // TODO:
|
||||
}
|
||||
};
|
||||
|
||||
// delay 500ms when deleting
|
||||
getFilter().setDelayer(new Filter.Delayer() {
|
||||
|
||||
private int mPreviousLength = 0;
|
||||
|
||||
public long getPostingDelay(CharSequence constraint) {
|
||||
if (constraint == null) return 0;
|
||||
|
||||
long delay = constraint.length() < mPreviousLength ? DELETE_KEY_POST_DELAY : 0;
|
||||
mPreviousLength = constraint.length();
|
||||
return delay;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden to always return <code>false</code>, since we cannot be sure that
|
||||
* suggestion sources return stable IDs.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
|
||||
String query = (constraint == null) ? "" : constraint.toString();
|
||||
/**
|
||||
* for in app search we show the progress spinner until the cursor is returned with
|
||||
* the results.
|
||||
*/
|
||||
Cursor cursor = null;
|
||||
//mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
|
||||
try {
|
||||
cursor = mSearchManager.getSuggestions(mSearchable, query, QUERY_LIMIT);
|
||||
// trigger fill window so the spinner stays up until the results are copied over and
|
||||
// closer to being ready
|
||||
if (cursor != null) {
|
||||
cursor.getCount();
|
||||
return cursor;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
|
||||
}
|
||||
// If cursor is null or an exception was thrown, stop the spinner and return null.
|
||||
// changeCursor doesn't get called if cursor is null
|
||||
// mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
|
||||
return null;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (DBG) Log.d(LOG_TAG, "close()");
|
||||
changeCursor(null);
|
||||
mClosed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyDataSetChanged() {
|
||||
if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged");
|
||||
super.notifyDataSetChanged();
|
||||
|
||||
// mSearchView.onDataSetChanged(); // TODO:
|
||||
|
||||
updateSpinnerState(getCursor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyDataSetInvalidated() {
|
||||
if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated");
|
||||
super.notifyDataSetInvalidated();
|
||||
|
||||
updateSpinnerState(getCursor());
|
||||
}
|
||||
|
||||
private void updateSpinnerState(Cursor cursor) {
|
||||
Bundle extras = cursor != null ? cursor.getExtras() : null;
|
||||
if (DBG) {
|
||||
Log.d(LOG_TAG, "updateSpinnerState - extra = "
|
||||
+ (extras != null
|
||||
? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
|
||||
: null));
|
||||
}
|
||||
// Check if the Cursor indicates that the query is not complete and show the spinner
|
||||
if (extras != null
|
||||
&& extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
|
||||
// mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
|
||||
return;
|
||||
}
|
||||
// If cursor is null or is done, stop the spinner
|
||||
// mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache columns.
|
||||
*/
|
||||
@Override
|
||||
public void changeCursor(Cursor c) {
|
||||
if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
|
||||
|
||||
if (mClosed) {
|
||||
Log.w(LOG_TAG, "Tried to change cursor after adapter was closed.");
|
||||
if (c != null) c.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
super.changeCursor(c);
|
||||
|
||||
if (c != null) {
|
||||
mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
|
||||
mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
|
||||
mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
|
||||
mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
|
||||
mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
|
||||
mBackgroundColorCol =
|
||||
c.getColumnIndex(SearchManager.SUGGEST_COLUMN_BACKGROUND_COLOR);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "error changing cursor and caching columns", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tags the view with cached child view look-ups.
|
||||
*/
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
View v = super.newView(context, cursor, parent);
|
||||
v.setTag(new ChildViewCache(v));
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache of the child views of drop-drown list items, to avoid looking up the children
|
||||
* each time the contents of a list item are changed.
|
||||
*/
|
||||
private final static class ChildViewCache {
|
||||
public final TextView mText1;
|
||||
public final TextView mText2;
|
||||
public final ImageView mIcon1;
|
||||
public final ImageView mIcon2;
|
||||
|
||||
public ChildViewCache(View v) {
|
||||
mText1 = (TextView) v.findViewById(com.android.internal.R.id.text1);
|
||||
mText2 = (TextView) v.findViewById(com.android.internal.R.id.text2);
|
||||
mIcon1 = (ImageView) v.findViewById(com.android.internal.R.id.icon1);
|
||||
mIcon2 = (ImageView) v.findViewById(com.android.internal.R.id.icon2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
ChildViewCache views = (ChildViewCache) view.getTag();
|
||||
|
||||
int backgroundColor = 0;
|
||||
if (mBackgroundColorCol != -1) {
|
||||
backgroundColor = cursor.getInt(mBackgroundColorCol);
|
||||
}
|
||||
Drawable background = getItemBackground(backgroundColor);
|
||||
view.setBackgroundDrawable(background);
|
||||
|
||||
if (views.mText1 != null) {
|
||||
String text1 = getStringOrNull(cursor, mText1Col);
|
||||
setViewText(views.mText1, text1);
|
||||
}
|
||||
if (views.mText2 != null) {
|
||||
// First check TEXT_2_URL
|
||||
CharSequence text2 = getStringOrNull(cursor, mText2UrlCol);
|
||||
if (text2 != null) {
|
||||
text2 = formatUrl(text2);
|
||||
} else {
|
||||
text2 = getStringOrNull(cursor, mText2Col);
|
||||
}
|
||||
|
||||
// If no second line of text is indicated, allow the first line of text
|
||||
// to be up to two lines if it wants to be.
|
||||
if (TextUtils.isEmpty(text2)) {
|
||||
if (views.mText1 != null) {
|
||||
views.mText1.setSingleLine(false);
|
||||
views.mText1.setMaxLines(2);
|
||||
}
|
||||
} else {
|
||||
if (views.mText1 != null) {
|
||||
views.mText1.setSingleLine(true);
|
||||
views.mText1.setMaxLines(1);
|
||||
}
|
||||
}
|
||||
setViewText(views.mText2, text2);
|
||||
}
|
||||
|
||||
if (views.mIcon1 != null) {
|
||||
setViewDrawable(views.mIcon1, getIcon1(cursor));
|
||||
}
|
||||
if (views.mIcon2 != null) {
|
||||
setViewDrawable(views.mIcon2, getIcon2(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence formatUrl(CharSequence url) {
|
||||
if (mUrlColor == null) {
|
||||
// Lazily get the URL color from the current theme.
|
||||
TypedValue colorValue = new TypedValue();
|
||||
mContext.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true);
|
||||
mUrlColor = mContext.getResources().getColorStateList(colorValue.resourceId);
|
||||
}
|
||||
|
||||
SpannableString text = new SpannableString(url);
|
||||
text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null),
|
||||
0, url.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a drawable with no color when selected or pressed, and the given color when
|
||||
* neither selected nor pressed.
|
||||
*
|
||||
* @return A drawable, or {@code null} if the given color is transparent.
|
||||
*/
|
||||
private Drawable getItemBackground(int backgroundColor) {
|
||||
if (backgroundColor == 0) {
|
||||
return null;
|
||||
} else {
|
||||
Drawable.ConstantState cachedBg = mBackgroundsCache.get(backgroundColor);
|
||||
if (cachedBg != null) {
|
||||
if (DBG) Log.d(LOG_TAG, "Background cache hit for color " + backgroundColor);
|
||||
return cachedBg.newDrawable(mProviderContext.getResources());
|
||||
}
|
||||
if (DBG) Log.d(LOG_TAG, "Creating new background for color " + backgroundColor);
|
||||
ColorDrawable transparent = new ColorDrawable(0);
|
||||
ColorDrawable background = new ColorDrawable(backgroundColor);
|
||||
StateListDrawable newBg = new StateListDrawable();
|
||||
newBg.addState(new int[]{android.R.attr.state_selected}, transparent);
|
||||
newBg.addState(new int[]{android.R.attr.state_pressed}, transparent);
|
||||
newBg.addState(new int[]{}, background);
|
||||
mBackgroundsCache.put(backgroundColor, newBg.getConstantState());
|
||||
return newBg;
|
||||
}
|
||||
}
|
||||
|
||||
private void setViewText(TextView v, CharSequence text) {
|
||||
// Set the text even if it's null, since we need to clear any previous text.
|
||||
v.setText(text);
|
||||
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
v.setVisibility(View.GONE);
|
||||
} else {
|
||||
v.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private Drawable getIcon1(Cursor cursor) {
|
||||
if (mIconName1Col < 0) {
|
||||
return null;
|
||||
}
|
||||
String value = cursor.getString(mIconName1Col);
|
||||
Drawable drawable = getDrawableFromResourceValue(value);
|
||||
if (drawable != null) {
|
||||
return drawable;
|
||||
}
|
||||
return getDefaultIcon1(cursor);
|
||||
}
|
||||
|
||||
private Drawable getIcon2(Cursor cursor) {
|
||||
if (mIconName2Col < 0) {
|
||||
return null;
|
||||
}
|
||||
String value = cursor.getString(mIconName2Col);
|
||||
return getDrawableFromResourceValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the drawable in an image view, makes sure the view is only visible if there
|
||||
* is a drawable.
|
||||
*/
|
||||
private void setViewDrawable(ImageView v, Drawable drawable) {
|
||||
// Set the icon even if the drawable is null, since we need to clear any
|
||||
// previous icon.
|
||||
v.setImageDrawable(drawable);
|
||||
|
||||
if (drawable == null) {
|
||||
v.setVisibility(View.GONE);
|
||||
} else {
|
||||
v.setVisibility(View.VISIBLE);
|
||||
|
||||
// This is a hack to get any animated drawables (like a 'working' spinner)
|
||||
// to animate. You have to setVisible true on an AnimationDrawable to get
|
||||
// it to start animating, but it must first have been false or else the
|
||||
// call to setVisible will be ineffective. We need to clear up the story
|
||||
// about animated drawables in the future, see http://b/1878430.
|
||||
drawable.setVisible(false, false);
|
||||
drawable.setVisible(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text to show in the query field when a suggestion is selected.
|
||||
*
|
||||
* @param cursor The Cursor to read the suggestion data from. The Cursor should already
|
||||
* be moved to the suggestion that is to be read from.
|
||||
* @return The text to show, or <code>null</code> if the query should not be
|
||||
* changed when selecting this suggestion.
|
||||
*/
|
||||
@Override
|
||||
public CharSequence convertToString(Cursor cursor) {
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);
|
||||
if (query != null) {
|
||||
return query;
|
||||
}
|
||||
|
||||
if (mSearchable.shouldRewriteQueryFromData()) {
|
||||
String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
|
||||
if (data != null) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
if (mSearchable.shouldRewriteQueryFromText()) {
|
||||
String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);
|
||||
if (text1 != null) {
|
||||
return text1;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is overridden purely to provide a bit of protection against
|
||||
* flaky content providers.
|
||||
*
|
||||
* @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(LOG_TAG, "Search suggestions cursor threw exception.", e);
|
||||
// Put exception string in item title
|
||||
View v = newView(mContext, mCursor, parent);
|
||||
if (v != null) {
|
||||
ChildViewCache views = (ChildViewCache) v.getTag();
|
||||
TextView tv = views.mText1;
|
||||
tv.setText(e.toString());
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a drawable given a value provided by a suggestion provider.
|
||||
*
|
||||
* This value could be just the string value of a resource id
|
||||
* (e.g., "2130837524"), in which case we will try to retrieve a drawable from
|
||||
* the provider's resources. If the value is not an integer, it is
|
||||
* treated as a Uri and opened with
|
||||
* {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
|
||||
*
|
||||
* All resources and URIs are read using the suggestion provider's context.
|
||||
*
|
||||
* If the string is not formatted as expected, or no drawable can be found for
|
||||
* the provided value, this method returns null.
|
||||
*
|
||||
* @param drawableId a string like "2130837524",
|
||||
* "android.resource://com.android.alarmclock/2130837524",
|
||||
* or "content://contacts/photos/253".
|
||||
* @return a Drawable, or null if none found
|
||||
*/
|
||||
private Drawable getDrawableFromResourceValue(String drawableId) {
|
||||
if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// First, see if it's just an integer
|
||||
int resourceId = Integer.parseInt(drawableId);
|
||||
// It's an int, look for it in the cache
|
||||
String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
+ "://" + mProviderContext.getPackageName() + "/" + resourceId;
|
||||
// Must use URI as cache key, since ints are app-specific
|
||||
Drawable drawable = checkIconCache(drawableUri);
|
||||
if (drawable != null) {
|
||||
return drawable;
|
||||
}
|
||||
// Not cached, find it by resource ID
|
||||
drawable = mProviderContext.getResources().getDrawable(resourceId);
|
||||
// Stick it in the cache, using the URI as key
|
||||
storeInIconCache(drawableUri, drawable);
|
||||
return drawable;
|
||||
} catch (NumberFormatException nfe) {
|
||||
// It's not an integer, use it as a URI
|
||||
Drawable drawable = checkIconCache(drawableId);
|
||||
if (drawable != null) {
|
||||
return drawable;
|
||||
}
|
||||
Uri uri = Uri.parse(drawableId);
|
||||
drawable = getDrawable(uri);
|
||||
storeInIconCache(drawableId, drawable);
|
||||
return drawable;
|
||||
} catch (Resources.NotFoundException nfe) {
|
||||
// It was an integer, but it couldn't be found, bail out
|
||||
Log.w(LOG_TAG, "Icon resource not found: " + drawableId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a drawable by URI, without using the cache.
|
||||
*
|
||||
* @return A drawable, or {@code null} if the drawable could not be loaded.
|
||||
*/
|
||||
private Drawable getDrawable(Uri uri) {
|
||||
try {
|
||||
String scheme = uri.getScheme();
|
||||
if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
|
||||
// Load drawables through Resources, to get the source density information
|
||||
OpenResourceIdResult r =
|
||||
mProviderContext.getContentResolver().getResourceId(uri);
|
||||
try {
|
||||
return r.r.getDrawable(r.id);
|
||||
} catch (Resources.NotFoundException ex) {
|
||||
throw new FileNotFoundException("Resource does not exist: " + uri);
|
||||
}
|
||||
} else {
|
||||
// Let the ContentResolver handle content and file URIs.
|
||||
InputStream stream = mProviderContext.getContentResolver().openInputStream(uri);
|
||||
if (stream == null) {
|
||||
throw new FileNotFoundException("Failed to open " + uri);
|
||||
}
|
||||
try {
|
||||
return Drawable.createFromStream(stream, null);
|
||||
} finally {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException ex) {
|
||||
Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException fnfe) {
|
||||
Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Drawable checkIconCache(String resourceUri) {
|
||||
Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri);
|
||||
if (cached == null) {
|
||||
return null;
|
||||
}
|
||||
if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri);
|
||||
return cached.newDrawable();
|
||||
}
|
||||
|
||||
private void storeInIconCache(String resourceUri, Drawable drawable) {
|
||||
if (drawable != null) {
|
||||
mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the left-hand side icon that will be used for the current suggestion
|
||||
* if the suggestion contains an icon column but no icon or a broken icon.
|
||||
*
|
||||
* @param cursor A cursor positioned at the current suggestion.
|
||||
* @return A non-null drawable.
|
||||
*/
|
||||
private Drawable getDefaultIcon1(Cursor cursor) {
|
||||
// First check the component that the suggestion is originally from
|
||||
String c = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME);
|
||||
if (c != null) {
|
||||
ComponentName component = ComponentName.unflattenFromString(c);
|
||||
if (component != null) {
|
||||
Drawable drawable = getActivityIconWithCache(component);
|
||||
if (drawable != null) {
|
||||
return drawable;
|
||||
}
|
||||
} else {
|
||||
Log.w(LOG_TAG, "Bad component name: " + c);
|
||||
}
|
||||
}
|
||||
|
||||
// Then check the component that gave us the suggestion
|
||||
Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity());
|
||||
if (drawable != null) {
|
||||
return drawable;
|
||||
}
|
||||
|
||||
// Fall back to a default icon
|
||||
return mContext.getPackageManager().getDefaultActivityIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the activity or application icon for an activity.
|
||||
* Uses the local icon cache for fast repeated lookups.
|
||||
*
|
||||
* @param component Name of an activity.
|
||||
* @return A drawable, or {@code null} if neither the activity nor the application
|
||||
* has an icon set.
|
||||
*/
|
||||
private Drawable getActivityIconWithCache(ComponentName component) {
|
||||
// First check the icon cache
|
||||
String componentIconKey = component.flattenToShortString();
|
||||
// Using containsKey() since we also store null values.
|
||||
if (mOutsideDrawablesCache.containsKey(componentIconKey)) {
|
||||
Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey);
|
||||
return cached == null ? null : cached.newDrawable(mProviderContext.getResources());
|
||||
}
|
||||
// Then try the activity or application icon
|
||||
Drawable drawable = getActivityIcon(component);
|
||||
// Stick it in the cache so we don't do this lookup again.
|
||||
Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState();
|
||||
mOutsideDrawablesCache.put(componentIconKey, toCache);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the activity or application icon for an activity.
|
||||
*
|
||||
* @param component Name of an activity.
|
||||
* @return A drawable, or {@code null} if neither the acitivy or the application
|
||||
* have an icon set.
|
||||
*/
|
||||
private Drawable getActivityIcon(ComponentName component) {
|
||||
PackageManager pm = mContext.getPackageManager();
|
||||
final ActivityInfo activityInfo;
|
||||
try {
|
||||
activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA);
|
||||
} catch (NameNotFoundException ex) {
|
||||
Log.w(LOG_TAG, ex.toString());
|
||||
return null;
|
||||
}
|
||||
int iconId = activityInfo.getIconResource();
|
||||
if (iconId == 0) return null;
|
||||
String pkg = component.getPackageName();
|
||||
Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo);
|
||||
if (drawable == null) {
|
||||
Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for "
|
||||
+ component.flattenToShortString());
|
||||
return null;
|
||||
}
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a string column by name.
|
||||
*
|
||||
* @param cursor Cursor to read the value from.
|
||||
* @param columnName The name of the column to read.
|
||||
* @return The value of the given column, or <code>null</null>
|
||||
* if the cursor does not contain the given column.
|
||||
*/
|
||||
public static String getColumnString(Cursor cursor, String columnName) {
|
||||
int col = cursor.getColumnIndex(columnName);
|
||||
return getStringOrNull(cursor, col);
|
||||
}
|
||||
|
||||
private static String getStringOrNull(Cursor cursor, int col) {
|
||||
if (col == NONE) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return cursor.getString(col);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG,
|
||||
"unexpected error retrieving valid column from cursor, "
|
||||
+ "did the remote process die?", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user