AI 147564: Merge back from search branch to donut. Notes:

- all public apis and framework changes have been reviewed by relevant folks in our branch (e.g romainguy)
  - all new public apis are @hidden; they will still get reviewed by api council once we're in git
  - other than that, it's mostly GlobalSearch and search dialog stuff, a new apps provider, and some tweaks
  to the contacts provider that was reviewed by jham

Automated import of CL 147564
This commit is contained in:
Karl Rosaen
2009-04-23 19:00:21 -07:00
committed by The Android Open Source Project
parent eeb4672371
commit 138a04170d
12 changed files with 459 additions and 852 deletions
+254 -625
View File
@@ -16,71 +16,59 @@
package com.android.launcher;
import android.app.ISearchManager;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.content.res.Configuration;
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.CursorAdapter;
import android.widget.Filter;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;
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 {
public class Search extends LinearLayout
implements OnClickListener, OnKeyListener, OnLongClickListener {
// Speed at which the widget slides up/down, in pixels/ms.
private static final float ANIMATION_VELOCITY = 1.0f;
private final String TAG = "SearchWidget";
private AutoCompleteTextView mSearchText;
private ImageButton mGoButton;
private Launcher mLauncher;
private TextView mSearchText;
private ImageButton mVoiceButton;
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;
/** The animation that morphs the search widget to the search dialog. */
private Animation mMorphAnimation;
/** The animation that morphs the search widget back to its normal position. */
private Animation mUnmorphAnimation;
// These four are passed to Launcher.startSearch() when the search widget
// has finished morphing. They are instance variables to make it possible to update
// them while the widget is morphing.
private String mInitialQuery;
private boolean mSelectInitialQuery;
private Bundle mAppSearchData;
private boolean mGlobalSearch;
// For voice searching
private Intent mVoiceSearchIntent;
private Rect mTempRect = new Rect();
private boolean mRestoreFocus = false;
/**
* Used to inflate the Workspace from XML.
*
@@ -89,293 +77,235 @@ public class Search extends LinearLayout implements OnClickListener, OnKeyListen
*/
public Search(Context context, AttributeSet attrs) {
super(context, attrs);
Interpolator interpolator = new AccelerateDecelerateInterpolator();
mMorphAnimation = new ToParentOriginAnimation();
// no need to apply transformation before the animation starts,
// since the gadget is already in its normal place.
mMorphAnimation.setFillBefore(false);
// stay in the top position after the animation finishes
mMorphAnimation.setFillAfter(true);
mMorphAnimation.setInterpolator(interpolator);
mMorphAnimation.setAnimationListener(new Animation.AnimationListener() {
// The amount of time before the animation ends to show the search dialog.
private static final long TIME_BEFORE_ANIMATION_END = 80;
// The runnable which we'll pass to our handler to show the search dialog.
private final Runnable mShowSearchDialogRunnable = new Runnable() {
public void run() {
showSearchDialog();
}
};
public void onAnimationEnd(Animation animation) { }
public void onAnimationRepeat(Animation animation) { }
public void onAnimationStart(Animation animation) {
// Make the search dialog show up ideally *just* as the animation reaches
// the top, to aid the illusion that the widget becomes the search dialog.
// Otherwise, there is a short delay when the widget reaches the top before
// the search dialog shows. We do this roughly 80ms before the animation ends.
getHandler().postDelayed(
mShowSearchDialogRunnable,
Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0));
}
});
mUnmorphAnimation = new FromParentOriginAnimation();
// stay in the top position until the animation starts
mUnmorphAnimation.setFillBefore(true);
// no need to apply transformation after the animation finishes,
// since the gadget is now back in its normal place.
mUnmorphAnimation.setFillAfter(false);
mUnmorphAnimation.setInterpolator(interpolator);
mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){
public void onAnimationEnd(Animation animation) {
clearAnimation();
}
public void onAnimationRepeat(Animation animation) { }
public void onAnimationStart(Animation animation) { }
});
mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL,
android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
}
/**
* Implements OnClickListener (for button)
* Implements OnClickListener.
*/
public void onClick(View v) {
if (v == mGoButton) {
query();
} else if (v == mVoiceButton) {
try {
getContext().startActivity(mVoiceSearchIntent);
} catch (ActivityNotFoundException ex) {
// Should not happen, since we check the availability of
// voice search before showing the button. But just in case...
Log.w(TAG, "Could not find voice search activity");
if (v == mVoiceButton) {
startVoiceSearch();
} else {
mLauncher.onSearchRequested();
}
}
private void startVoiceSearch() {
try {
getContext().startActivity(mVoiceSearchIntent);
} catch (ActivityNotFoundException ex) {
// Should not happen, since we check the availability of
// voice search before showing the button. But just in case...
Log.w(TAG, "Could not find voice search activity");
}
}
/**
* Sets the query text. The query field is not editable, instead we forward
* the key events to the launcher, which keeps track of the text,
* calls setQuery() to show it, and gives it to the search dialog.
*/
public void setQuery(String query) {
mSearchText.setText(query, TextView.BufferType.NORMAL);
}
/**
* Morph the search gadget to the search dialog.
* See {@link Activity.startSearch()} for the arguments.
*/
public void startSearch(String initialQuery, boolean selectInitialQuery,
Bundle appSearchData, boolean globalSearch) {
mInitialQuery = initialQuery;
mSelectInitialQuery = selectInitialQuery;
mAppSearchData = appSearchData;
mGlobalSearch = globalSearch;
// Call up the keyboard before we actually call the search dialog so that it
// (hopefully) animates in at about the same time as the widget animation, and
// so that it becomes available as soon as possible. Only do this if a hard
// keyboard is not currently available.
if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
Configuration.HARDKEYBOARDHIDDEN_YES) {
// Make sure the text field is not focusable, so it's not responsible for
// causing the whole view to shift up to accommodate the keyboard.
mSearchText.setFocusable(false);
InputMethodManager inputManager = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.showSoftInputUnchecked(0, null);
}
if (isAtTop()) {
showSearchDialog();
} else {
// Start the animation, unless it has already started.
if (getAnimation() != mMorphAnimation) {
mMorphAnimation.setDuration(getAnimationDuration());
startAnimation(mMorphAnimation);
}
}
}
private void query() {
String query = mSearchText.getText().toString();
if (TextUtils.getTrimmedLength(mSearchText.getText()) == 0) {
return;
/**
* Shows the system search dialog immediately, without any animation.
*/
private void showSearchDialog() {
mLauncher.showSearchDialog(
mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch);
}
/**
* Restore the search gadget to its normal position.
*
* @param animate Whether to animate the movement of the gadget.
*/
public void stopSearch(boolean animate) {
setQuery("");
// Set the search field back to focusable after making it unfocusable in
// startSearch, so that the home screen doesn't try to shift around when the
// keyboard comes up.
mSearchText.setFocusable(true);
// Only restore if we are not already restored.
if (getAnimation() == mMorphAnimation) {
if (animate && !isAtTop()) {
mUnmorphAnimation.setDuration(getAnimationDuration());
startAnimation(mUnmorphAnimation);
} else {
clearAnimation();
}
}
}
private boolean isAtTop() {
return getTop() == 0;
}
private int getAnimationDuration() {
return (int) (getTop() / ANIMATION_VELOCITY);
}
/**
* Modify clearAnimation() to invalidate the parent. This works around
* an issue where the region where the end of the animation placed the view
* was not redrawn after clearing the animation.
*/
@Override
public void clearAnimation() {
Animation animation = getAnimation();
if (animation != null) {
super.clearAnimation();
if (animation.hasEnded()
&& animation.getFillAfter()
&& animation.willChangeBounds()) {
((View) getParent()).invalidate();
} else {
invalidate();
}
}
Bundle appData = new Bundle();
appData.putString(SearchManager.SOURCE, "launcher-widget");
sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, 0, null, mSearchable);
clearQuery();
}
/**
* 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);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (!hasWindowFocus && hasFocus()) {
mRestoreFocus = true;
}
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus && mRestoreFocus) {
if (isInTouchMode()) {
final AutoCompleteTextView searchText = mSearchText;
searchText.setSelectAllOnFocus(false);
searchText.requestFocusFromTouch();
searchText.setSelectAllOnFocus(true);
public boolean onKey(View v, int keyCode, KeyEvent event) {
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)) {
// Forward key events to Launcher, which will forward text
// to search dialog
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
return mLauncher.onKeyDown(keyCode, event);
case KeyEvent.ACTION_MULTIPLE:
return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event);
case KeyEvent.ACTION_UP:
return mLauncher.onKeyUp(keyCode, event);
}
mRestoreFocus = false;
}
}
/**
* 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 || v == mVoiceButton) {
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)
* Implements OnLongClickListener to pass long clicks on child views
* to the widget. This makes it possible to pick up the widget by long
* clicking on the text field or a 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) {
// Request focus unless the user tapped on the voice search button
final int x = (int) ev.getX();
final int y = (int) ev.getY();
final Rect frame = mTempRect;
mVoiceButton.getHitRect(frame);
if (!frame.contains(x, y)) {
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);
return performLongClick();
}
@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 = (ImageButton) findViewById(R.id.search_go_btn);
mSearchText = (TextView) findViewById(R.id.search_src_text);
mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn);
mGoButton.setOnClickListener(this);
mSearchText.setOnKeyListener(this);
mSearchText.setOnClickListener(this);
mVoiceButton.setOnClickListener(this);
mGoButton.setOnKeyListener(this);
mVoiceButton.setOnKeyListener(this);
setOnClickListener(this);
mSearchText.setOnLongClickListener(this);
mGoButton.setOnLongClickListener(this);
mVoiceButton.setOnLongClickListener(this);
// disable the button since we start out w/empty input
mGoButton.setEnabled(false);
mGoButton.setFocusable(false);
configureSearchableInfo();
configureSuggestions();
configureVoiceSearchButton();
}
/**
* Cache of popup padding value after read from {@link Resources}.
*/
private static float mPaddingInset = -1;
/**
* When our size is changed, pass down adjusted width and offset values to
* correctly center the {@link AutoCompleteTextView} popup and include our
* padding.
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
if (mPaddingInset == -1) {
mPaddingInset = getResources().getDimension(R.dimen.search_widget_inset);
}
// Fill entire width of widget, minus padding inset
float paddedWidth = getWidth() - (mPaddingInset * 2);
float paddedOffset = -(mSearchText.getLeft() - mPaddingInset);
mSearchText.setDropDownWidth((int) paddedWidth);
mSearchText.setDropDownHorizontalOffset((int) paddedOffset);
}
}
/**
* Read the searchable info from the search manager
*/
private void configureSearchableInfo() {
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;
}
/**
* If appropriate & available, configure voice search
*
@@ -384,346 +314,45 @@ public class Search extends LinearLayout implements OnClickListener, OnKeyListen
* voice search.
*/
private void configureVoiceSearchButton() {
boolean voiceSearchVisible = false;
if (mSearchable.getVoiceSearchEnabled() && mSearchable.getVoiceSearchLaunchWebSearch()) {
// Enable the voice search button if there is an activity that can handle it
PackageManager pm = getContext().getPackageManager();
ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
PackageManager.MATCH_DEFAULT_ONLY);
voiceSearchVisible = ri != null;
}
// Enable the voice search button if there is an activity that can handle it
PackageManager pm = getContext().getPackageManager();
ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
PackageManager.MATCH_DEFAULT_ONLY);
boolean voiceSearchVisible = ri != null;
// finally, set visible state of voice search button, as appropriate
mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE);
}
/** The rest of the class deals with providing search suggestions */
/**
* Set up the suggestions provider mechanism
* Sets the {@link Launcher} that this gadget will call on to display the search dialog.
*/
private void configureSuggestions() {
// get SearchableInfo
mSearchText.setOnItemClickListener(this);
mSearchText.setOnItemSelectedListener(this);
// attach the suggestions adapter
mSuggestionsAdapter = new SuggestionsAdapter(mContext,
com.android.internal.R.layout.search_dropdown_item_2line, null,
SuggestionsAdapter.TWO_LINE_FROM, SuggestionsAdapter.TWO_LINE_TO, mSearchable);
mSearchText.setAdapter(mSuggestionsAdapter);
public void setLauncher(Launcher launcher) {
mLauncher = launcher;
}
/**
* Remove internal cursor references when detaching from window which
* prevents {@link Context} leaks.
*/
@Override
public void onDetachedFromWindow() {
if (mSuggestionsAdapter != null) {
mSuggestionsAdapter.changeCursor(null);
mSuggestionsAdapter = null;
}
}
/**
* Implements OnItemClickListener
*/
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Log.d(TAG, "onItemClick() position " + position);
launchSuggestion(mSuggestionsAdapter, position);
}
/**
* Implements OnItemSelectedListener
* Moves the view to the top left corner of its parent.
*/
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);
clearQuery();
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());
}
}
SearchAutoCompleteTextView getSearchInputField() {
return (SearchAutoCompleteTextView) mSearchText;
}
/**
* This class provides the filtering-based interface to suggestions providers.
* It is hardwired in a couple of places to support GoogleSearch - for example, it supports
* two-line suggestions, but it does not support icons.
*/
private static class SuggestionsAdapter extends SimpleCursorAdapter {
public final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2 };
public final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1,
com.android.internal.R.id.text2};
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.
*/
private class ToParentOriginAnimation extends Animation {
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
String query = (constraint == null) ? "" : constraint.toString();
return getSuggestions(mSearchable, query);
protected void applyTransformation(float interpolatedTime, Transformation t) {
float dx = -getLeft() * interpolatedTime;
float dy = -getTop() * interpolatedTime;
t.getMatrix().setTranslate(dx, dy);
}
/**
* 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;
}
}
}
/**
* Moves the view from the top left corner of its parent.
*/
private class FromParentOriginAnimation extends Animation {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float dx = -getLeft() * (1.0f - interpolatedTime);
float dy = -getTop() * (1.0f - interpolatedTime);
t.getMatrix().setTranslate(dx, dy);
}
}
}