summaryrefslogtreecommitdiffstats
path: root/core/java/android/app/SearchDialog.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/app/SearchDialog.java')
-rw-r--r--core/java/android/app/SearchDialog.java960
1 files changed, 392 insertions, 568 deletions
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java
index 2ce2db9..f1c604c 100644
--- a/core/java/android/app/SearchDialog.java
+++ b/core/java/android/app/SearchDialog.java
@@ -16,14 +16,11 @@
package android.app;
-import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
@@ -38,6 +35,7 @@ import android.os.ServiceManager;
import android.os.SystemClock;
import android.server.search.SearchableInfo;
import android.text.Editable;
+import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
@@ -45,21 +43,13 @@ import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
-import android.view.View.OnFocusChangeListener;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.view.inputmethod.InputMethodManager;
-import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
import android.widget.CursorAdapter;
-import android.widget.EditText;
-import android.widget.ImageButton;
import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
@@ -67,8 +57,7 @@ import android.widget.WrapperListAdapter;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
-import java.util.List;
-import java.util.concurrent.LinkedBlockingQueue;
+import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -77,7 +66,7 @@ import java.util.concurrent.atomic.AtomicLong;
*
* @hide
*/
-public class SearchDialog extends Dialog {
+public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
// Debugging support
final static String LOG_TAG = "SearchDialog";
@@ -87,7 +76,6 @@ public class SearchDialog extends Dialog {
// interaction with runtime
IntentFilter mCloseDialogsFilter;
IntentFilter mPackageFilter;
- private final Handler mHandler = new Handler(); // why isn't Dialog.mHandler shared?
private static final String INSTANCE_KEY_COMPONENT = "comp";
private static final String INSTANCE_KEY_APPDATA = "data";
@@ -102,15 +90,10 @@ public class SearchDialog extends Dialog {
private static final int INSTANCE_SELECTED_QUERY = -1;
// views & widgets
- private View mSearchBarLayout;
private TextView mBadgeLabel;
- private LinearLayout mSearchEditLayout;
- private EditText mSearchTextField;
- private ImageButton mGoButton;
- private ListView mSuggestionsList;
+ private AutoCompleteTextView mSearchTextField;
+ private Button mGoButton;
- private ViewTreeObserver mViewTreeObserver = null;
-
// interaction with searchable application
private ComponentName mLaunchComponent;
private Bundle mAppSearchData;
@@ -121,22 +104,20 @@ public class SearchDialog extends Dialog {
private SearchableInfo mSearchable;
// support for suggestions
- private SuggestionsRunner mSuggestionsRunner;
private String mUserQuery = null;
private int mUserQuerySelStart;
private int mUserQuerySelEnd;
- private boolean mNonUserQuery = false;
private boolean mLeaveJammedQueryOnRefocus = false;
private String mPreviousSuggestionQuery = null;
- private Context mProviderContext;
- private Animation mSuggestionsEntry;
- private Animation mSuggestionsExit;
- private boolean mSkipNextAnimate;
private int mPresetSelection = -1;
private String mSuggestionAction = null;
private Uri mSuggestionData = null;
private String mSuggestionQuery = null;
+ // support for AutoCompleteTextView suggestions display
+ private SuggestionsAdapter mSuggestionsAdapter;
+
+
/**
* Constructor - fires it up and makes it look like the search UI.
*
@@ -167,38 +148,25 @@ public class SearchDialog extends Dialog {
theWindow.setAttributes(lp);
// get the view elements for local access
- mSearchBarLayout = findViewById(com.android.internal.R.id.search_bar);
mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
- mSearchEditLayout = (LinearLayout)findViewById(com.android.internal.R.id.search_edit_frame);
- mSearchTextField = (EditText) findViewById(com.android.internal.R.id.search_src_text);
- mGoButton = (ImageButton) findViewById(com.android.internal.R.id.search_go_btn);
- mSuggestionsList = (ListView) findViewById(com.android.internal.R.id.search_suggest_list);
+ mSearchTextField = (AutoCompleteTextView)
+ findViewById(com.android.internal.R.id.search_src_text);
+ mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
// attach listeners
mSearchTextField.addTextChangedListener(mTextWatcher);
mSearchTextField.setOnKeyListener(mTextKeyListener);
mGoButton.setOnClickListener(mGoButtonClickListener);
mGoButton.setOnKeyListener(mButtonsKeyListener);
- mSuggestionsList.setOnItemClickListener(mSuggestionsListItemClickListener);
- mSuggestionsList.setOnKeyListener(mSuggestionsKeyListener);
- mSuggestionsList.setOnFocusChangeListener(mSuggestFocusListener);
- mSuggestionsList.setOnItemSelectedListener(mSuggestSelectedListener);
// pre-hide all the extraneous elements
mBadgeLabel.setVisibility(View.GONE);
- mSuggestionsList.setVisibility(View.GONE);
// Additional adjustments to make Dialog work for Search
// Touching outside of the search dialog will dismiss it
setCanceledOnTouchOutside(true);
- // Preload animations
- mSuggestionsEntry = AnimationUtils.loadAnimation(getContext(),
- com.android.internal.R.anim.grow_fade_in);
- mSuggestionsExit = AnimationUtils.loadAnimation(getContext(),
- com.android.internal.R.anim.fade_out);
-
// Set up broadcast filters
mCloseDialogsFilter = new
IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
@@ -238,18 +206,10 @@ public class SearchDialog extends Dialog {
}
// OK, we're going to show ourselves
- if (mSuggestionsList != null) {
- mSuggestionsList.setVisibility(View.GONE); // prevent any flicker if was visible
- }
-
super.show();
setupSearchableInfo();
- // start the suggestions thread (which will mainly idle)
- mSuggestionsRunner = new SuggestionsRunner();
- new Thread(mSuggestionsRunner, "SearchSuggestions").start();
-
mLaunchComponent = componentName;
mAppSearchData = appSearchData;
mGlobalSearchMode = globalSearch;
@@ -258,26 +218,21 @@ public class SearchDialog extends Dialog {
getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
- mViewTreeObserver = mSearchBarLayout.getViewTreeObserver();
- mViewTreeObserver.addOnGlobalLayoutListener(mGlobalLayoutListener);
+ // configure the autocomplete aspects of the input box
+ mSearchTextField.setOnItemClickListener(this);
+ mSearchTextField.setOnItemSelectedListener(this);
+
+ // attach the suggestions adapter
+ mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable);
+ mSearchTextField.setAdapter(mSuggestionsAdapter);
// finally, load the user's initial text (which may trigger suggestions)
- mNonUserQuery = false;
+ mSuggestionsAdapter.setNonUserQuery(false);
if (initialQuery == null) {
initialQuery = ""; // This forces the preload to happen, triggering suggestions
}
mSearchTextField.setText(initialQuery);
- // If it is not for global search, that means the search dialog is
- // launched to input a web address.
- if (!globalSearch) {
- mSearchTextField.setRawInputType(EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_VARIATION_URI);
- } else {
- mSearchTextField.setRawInputType(EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_VARIATION_NORMAL);
- }
-
if (selectInitialQuery) {
mSearchTextField.selectAll();
} else {
@@ -315,21 +270,7 @@ public class SearchDialog extends Dialog {
// This is OK - it just means we didn't have any registered
}
- // ignore layout notifications
- try {
- if (mViewTreeObserver != null) {
- mViewTreeObserver.removeGlobalOnLayoutListener(mGlobalLayoutListener);
- }
- } catch (RuntimeException e) {
- // This is OK - none registered or observer "dead"
- }
- mViewTreeObserver = null;
-
// dump extra memory we're hanging on to
- if (mSuggestionsRunner != null) {
- mSuggestionsRunner.cancelSuggestions();
- mSuggestionsRunner = null;
- }
mLaunchComponent = null;
mAppSearchData = null;
mSearchable = null;
@@ -337,7 +278,6 @@ public class SearchDialog extends Dialog {
mSuggestionData = null;
mSuggestionQuery = null;
mActivityContext = null;
- mProviderContext = null;
mPreviousSuggestionQuery = null;
mUserQuery = null;
}
@@ -366,9 +306,8 @@ public class SearchDialog extends Dialog {
int selectedElement = INSTANCE_SELECTED_QUERY;
if (mGoButton.isFocused()) {
selectedElement = INSTANCE_SELECTED_BUTTON;
- } else if ((mSuggestionsList.getVisibility() == View.VISIBLE) &&
- mSuggestionsList.isFocused()) {
- selectedElement = mSuggestionsList.getSelectedItemPosition(); // 0..n
+ } else if (mSearchTextField.isPopupShowing()) {
+ selectedElement = 0; // TODO mSearchTextField.getListSelection() // 0..n
}
bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement);
@@ -403,11 +342,13 @@ public class SearchDialog extends Dialog {
// for some reason, we couldn't re-instantiate
return;
}
- mSkipNextAnimate = true;
- mNonUserQuery = true;
+ mSuggestionsAdapter.setNonUserQuery(true);
mSearchTextField.setText(displayQuery);
- mNonUserQuery = false;
+ // TODO because the new query is (not) processed in another thread, we can't just
+ // take away this flag (yet). The better solution here is going to require a new API
+ // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
+// mSuggestionsAdapter.setNonUserQuery(false);
// clean up the selection state
switch (selectedElement) {
@@ -425,6 +366,7 @@ public class SearchDialog extends Dialog {
default:
// defer selecting a list element until suggestion list appears
mPresetSelection = selectedElement;
+ // TODO mSearchTextField.setListSelection(selectedElement)
break;
}
}
@@ -436,6 +378,7 @@ public class SearchDialog extends Dialog {
public void onConfigurationChanged(Configuration newConfig) {
if (isShowing()) {
// Redraw (resources may have changed)
+ updateSearchButton();
updateSearchBadge();
updateQueryHint();
}
@@ -448,10 +391,28 @@ public class SearchDialog extends Dialog {
private void setupSearchableInfo() {
if (mSearchable != null) {
mActivityContext = mSearchable.getActivityContext(getContext());
- mProviderContext = mSearchable.getProviderContext(getContext(), mActivityContext);
+ updateSearchButton();
updateSearchBadge();
updateQueryHint();
+
+ // In order to properly configure the input method (if one is being used), we
+ // need to let it know if we'll be providing suggestions. Although it would be
+ // difficult/expensive to know if every last detail has been configured properly, we
+ // can at least see if a suggestions provider has been configured, and use that
+ // as our trigger.
+ int inputType = mSearchable.getInputType();
+ // We only touch this if the input type is set up for text (which it almost certainly
+ // should be, in the case of search!)
+ if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
+ // The existence of a suggestions authority is the proxy for "suggestions
+ // are available here"
+ inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ if (mSearchable.getSuggestAuthority() != null) {
+ inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+ }
+ }
+ mSearchTextField.setInputType(inputType);
}
}
@@ -468,6 +429,24 @@ public class SearchDialog extends Dialog {
cancel();
}
+ /**
+ * Update the text in the search button. Note: This is deprecated functionality, for
+ * 1.0 compatibility only.
+ */
+ private void updateSearchButton() {
+ String textLabel = null;
+ Drawable iconLabel = null;
+ int textId = mSearchable.getSearchButtonText();
+ if (textId != 0) {
+ textLabel = mActivityContext.getResources().getString(textId);
+ } else {
+ iconLabel = getContext().getResources().
+ getDrawable(com.android.internal.R.drawable.ic_btn_search);
+ }
+ mGoButton.setText(textLabel);
+ mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
+ }
+
/**
* Setup the search "Badge" if request by mode flags.
*/
@@ -559,8 +538,8 @@ public class SearchDialog extends Dialog {
}
updateWidgetState();
// Only do suggestions if actually typed by user
- if (!mNonUserQuery) {
- updateSuggestions();
+ if (mSuggestionsAdapter.getNonUserQuery()) {
+ mPreviousSuggestionQuery = s.toString();
mUserQuery = mSearchTextField.getText().toString();
mUserQuerySelStart = mSearchTextField.getSelectionStart();
mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
@@ -582,156 +561,6 @@ public class SearchDialog extends Dialog {
mGoButton.setFocusable(enabled);
}
- /**
- * In response to a change in the query text, update the suggestions
- */
- private void updateSuggestions() {
- final String queryText = mSearchTextField.getText().toString();
- mPreviousSuggestionQuery = queryText;
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("updateSuggestions()");
- }
-
- mSuggestionsRunner.requestSuggestions(mSearchable, queryText);
-
- // For debugging purposes, put in a lot of strings (really fast typist)
- if (DBG_JAM_THREADING > 0) {
- for (int ii = 1; ii < DBG_JAM_THREADING; ++ii) {
- final String jamQuery = queryText + ii;
- mSuggestionsRunner.requestSuggestions(mSearchable, jamQuery);
- }
- // one final (correct) string for cleanup
- mSuggestionsRunner.requestSuggestions(mSearchable, queryText);
- }
- }
-
- /**
- * This class defines a queued message structure for processing user keystrokes, and a
- * thread that allows the suggestions to be gathered out-of-band, and allows us to skip
- * over multiple keystrokes if the typist is faster than the content provider.
- */
- private class SuggestionsRunner implements Runnable {
-
- private class Request {
- final SearchableInfo mSearchableInfo; // query will set these
- final String mQueryText;
- final boolean cancelRequest; // cancellation will set this
-
- // simple constructors
- Request(final SearchableInfo searchable, final String queryText) {
- mSearchableInfo = searchable;
- mQueryText = queryText;
- cancelRequest = false;
- }
-
- Request() {
- mSearchableInfo = null;
- mQueryText = null;
- cancelRequest = true;
- }
- }
-
- private final LinkedBlockingQueue<Request> mSuggestionsQueue =
- new LinkedBlockingQueue<Request>();
-
- /**
- * Queue up a suggestions request (non-blocking - can safely call from UI thread)
- */
- public void requestSuggestions(final SearchableInfo searchable, final String queryText) {
- Request request = new Request(searchable, queryText);
- try {
- mSuggestionsQueue.put(request);
- } catch (InterruptedException e) {
- // discard the request.
- }
- }
-
- /**
- * Cancel blocking suggestions, discard any results, and shut down the thread.
- * (non-blocking - can safely call from UI thread)
- */
- private void cancelSuggestions() {
- Request request = new Request();
- try {
- mSuggestionsQueue.put(request);
- } catch (InterruptedException e) {
- // discard the request.
- // TODO can we do better here?
- }
- }
-
- /**
- * This runnable implements the logic for decoupling keystrokes from suggestions.
- * The logic isn't quite obvious here, so I'll try to describe it.
- *
- * Normally we simply sleep waiting for a keystroke. When a keystroke arrives,
- * we immediately dispatch a request to gather suggestions.
- *
- * But this can take a while, so by the time it comes back, more keystrokes may have
- * arrived. If anything happened while we were gathering the suggestion, we discard its
- * results, and then use the most recent keystroke to start the next suggestions request.
- *
- * Any request containing cancelRequest == true will cause the thread to immediately
- * terminate.
- */
- public void run() {
- // outer blocking loop simply waits for a suggestion
- while (true) {
- try {
- Request request = mSuggestionsQueue.take();
- if (request.cancelRequest) {
- return;
- }
-
- // since we were idle, what we're really interested is the final element
- // in the queue. So keep pulling until we get the last element.
- // TODO Could we just do some sort of takeHead() here?
- while (! mSuggestionsQueue.isEmpty()) {
- request = mSuggestionsQueue.take();
- if (request.cancelRequest) {
- return;
- }
- }
- final Request useRequest = request;
-
- // now process the final element (unless it's a cancel - that can be discarded)
-
- if (useRequest.mSearchableInfo != null) {
-
- // go get the cursor. this is what takes time.
- final Cursor c = getSuggestions(useRequest.mSearchableInfo,
- useRequest.mQueryText);
-
- // We now have a suggestions result. But, if any new requests have arrived,
- // we're going to discard them - we don't want to waste time displaying
- // out-of-date results, we just want to get going on the next set.
- // Note, null cursor is a valid result (no suggestions). This logic also
- // supports the need to discard the results *and* stop the thread if a kill
- // request arrives during a query.
- if (mSuggestionsQueue.size() > 0) {
- if (c != null) {
- c.close();
- }
- } else {
- mHandler.post(new Runnable() {
- public void run() {
- updateSuggestionsWithCursor(c, useRequest.mSearchableInfo);
- }
- });
- }
- }
- } catch (InterruptedException e) {
- // loop back for more
- }
- // At this point the queue may contain zero-to-many new requests; We simply
- // loop back to handle them (or, block until new requests arrive)
- }
- }
- }
-
- /**
- * Back in the UI thread, handle incoming cursors
- */
private final static String[] ONE_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1 };
private final static String[] ONE_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_ICON_1,
@@ -755,209 +584,6 @@ public class SearchDialog extends Dialog {
com.android.internal.R.id.icon2};
/**
- * A new cursor (with suggestions) is ready for use. Update the UI.
- */
- void updateSuggestionsWithCursor(Cursor c, final SearchableInfo searchable) {
- ListAdapter adapter = null;
-
- // first, check for various conditions that disqualify this cursor
- if ((c == null) || (c.getCount() == 0)) {
- // no cursor, or cursor with no data
- } else if ((searchable != mSearchable) || !isShowing()) {
- // race condition (suggestions arrived after conditions changed)
- } else {
- // check cursor before trying to create list views from it
- int colId = c.getColumnIndex("_id");
- int col1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
- int col2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
- int colIc1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
- int colIc2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
-
- boolean minimal = (colId >= 0) && (col1 >= 0);
- boolean hasIcons = (colIc1 >= 0) && (colIc2 >= 0);
- boolean has2Lines = col2 >= 0;
-
- if (minimal) {
- int layout;
- String[] from;
- int[] to;
-
- if (hasIcons) {
- if (has2Lines) {
- layout = com.android.internal.R.layout.search_dropdown_item_icons_2line;
- from = TWO_LINE_ICONS_FROM;
- to = TWO_LINE_ICONS_TO;
- } else {
- layout = com.android.internal.R.layout.search_dropdown_item_icons_1line;
- from = ONE_LINE_ICONS_FROM;
- to = ONE_LINE_ICONS_TO;
- }
- } else {
- if (has2Lines) {
- layout = com.android.internal.R.layout.search_dropdown_item_2line;
- from = TWO_LINE_FROM;
- to = TWO_LINE_TO;
- } else {
- layout = com.android.internal.R.layout.search_dropdown_item_1line;
- from = ONE_LINE_FROM;
- to = ONE_LINE_TO;
- }
- }
- try {
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("updateSuggestions(3)");
- }
- adapter = new SuggestionsCursorAdapter(getContext(), layout, c, from, to,
- mProviderContext);
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("updateSuggestions(4)");
- }
- } catch (RuntimeException e) {
- Log.e(LOG_TAG, "Exception while creating SuggestionsCursorAdapter", e);
- }
- }
-
- // Provide some help for developers instead of just silently discarding
- if ((colIc1 >= 0) != (colIc2 >= 0)) {
- Log.w(LOG_TAG, "Suggestion icon column(s) discarded, must be 0 or 2 columns.");
- } else if (adapter == null) {
- Log.w(LOG_TAG, "Suggestions cursor discarded due to missing required columns.");
- }
- }
-
- // if we have a cursor but we're not using it (e.g. disqualified), close it now
- if ((c != null) && (adapter == null)) {
- c.close();
- c = null;
- }
-
- // we only made an adapter if there were 1+ suggestions. Now, based on the existence
- // of the adapter, we'll also show/hide the list.
- discardListCursor(mSuggestionsList);
- if (adapter == null) {
- showSuggestions(false, !mSkipNextAnimate);
- } else {
- layoutSuggestionsList();
- showSuggestions(true, !mSkipNextAnimate);
- }
- mSkipNextAnimate = false;
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("updateSuggestions(5)");
- }
- mSuggestionsList.setAdapter(adapter);
- // now that we have an adapter, we can actually adjust the selection & scroll positions
- if (mPresetSelection >= 0) {
- boolean bTouchMode = mSuggestionsList.isInTouchMode();
- mSuggestionsList.setSelection(mPresetSelection);
- mPresetSelection = -1;
- }
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("updateSuggestions(6)");
- }
- }
-
- /**
- * Utility for showing & hiding the suggestions list. This is also responsible for triggering
- * animation, if any, at the right time.
- *
- * @param visible If true, show the suggestions, if false, hide them.
- * @param animate If true, use animation. If false, "just do it."
- */
- private void showSuggestions(boolean visible, boolean animate) {
- if (visible) {
- if (animate && (mSuggestionsList.getVisibility() != View.VISIBLE)) {
- mSuggestionsList.startAnimation(mSuggestionsEntry);
- }
- mSuggestionsList.setVisibility(View.VISIBLE);
- } else {
- if (animate && (mSuggestionsList.getVisibility() != View.GONE)) {
- mSuggestionsList.startAnimation(mSuggestionsExit);
- }
- mSuggestionsList.setVisibility(View.GONE);
- }
- }
-
- /**
- * This helper class supports the suggestions list by allowing 3rd party (e.g. app) resources
- * to be used in suggestions
- */
- private static class SuggestionsCursorAdapter extends SimpleCursorAdapter {
-
- private Resources mProviderResources;
-
- public SuggestionsCursorAdapter(Context context, int layout, Cursor c,
- String[] from, int[] to, Context providerContext) {
- super(context, layout, c, from, to);
- mProviderResources = providerContext.getResources();
- }
-
- /**
- * 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).
- *
- * @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.
- */
- @Override
- /**
- * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
- */
- 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 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;
- }
- }
- }
-
- /**
- * Cleanly close the cursor being used by a ListView. Do this before replacing the adapter
- * or before closing the ListView.
- */
- private void discardListCursor(ListView list) {
- CursorAdapter ca = getSuggestionsAdapter(list);
- if (ca != null) {
- Cursor c = ca.getCursor();
- if (c != null) {
- ca.changeCursor(null);
- }
- }
- }
-
- /**
* Safely retrieve the suggestions cursor adapter from the ListView
*
* @param adapterView The ListView containing our adapter
@@ -977,58 +603,6 @@ public class SearchDialog extends Dialog {
}
/**
- * Get the query cursor for the search suggestions.
- *
- * @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) { // if selection provided, use it
- selArgs = new String[] {query};
- } else {
- uriStr.append('/'); // no sel, use REST pattern
- uriStr.append(Uri.encode(query));
- }
-
- // finally, make the query
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("getSuggestions(1)");
- }
- cursor = getContext().getContentResolver().query(
- Uri.parse(uriStr.toString()), null,
- searchable.getSuggestSelection(), selArgs,
- null);
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("getSuggestions(2)");
- }
- } catch (RuntimeException e) {
- Log.w(LOG_TAG, "Search Suggestions query returned exception " + e.toString());
- cursor = null;
- }
- }
-
- return cursor;
- }
-
- /**
* React to typing in the GO search button by refocusing to EditText.
* Continue typing the query.
*/
@@ -1094,7 +668,10 @@ public class SearchDialog extends Dialog {
* React to the user typing while the suggestions are focused. First, check for action
* keys. If not handled, try refocusing regular characters into the EditText. In this case,
* replace the query text (start typing fresh text).
+ *
+ * TODO: Move this code into mTextKeyListener, testing for a list entry being hilited
*/
+ /*
View.OnKeyListener mSuggestionsKeyListener = new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
boolean handled = false;
@@ -1108,6 +685,7 @@ public class SearchDialog extends Dialog {
return handled;
}
};
+ */
/**
* Per UI design, we're going to "steer" any typed keystrokes back into the EditText
@@ -1140,6 +718,9 @@ public class SearchDialog extends Dialog {
/**
* Update query text based on transitions in and out of suggestions list.
*/
+ /*
+ * TODO - figure out if this logic is required for the autocomplete text view version
+
OnFocusChangeListener mSuggestFocusListener = new OnFocusChangeListener() {
public void onFocusChange(View v, boolean hasFocus) {
// also guard against possible race conditions (late arrival after dismiss)
@@ -1170,24 +751,9 @@ public class SearchDialog extends Dialog {
}
};
+ */
/**
- * Update query text based on movement of selection in/out of suggestion list
- */
- OnItemSelectedListener mSuggestSelectedListener = new OnItemSelectedListener() {
- public void onItemSelected(AdapterView parent, View view, int position, long id) {
- // Update query text while user navigates through suggestions list
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null && position >= 0 && mSuggestionsList.isFocused()) {
- jamSuggestionQuery(true, parent, position);
- }
- }
-
- // No action needed on this callback
- public void onNothingSelected(AdapterView parent) { }
- };
-
- /**
* This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent. It's an indication that
* we should close ourselves immediately, in order to allow a higher-priority UI to take over
* (e.g. phone call received).
@@ -1225,20 +791,6 @@ public class SearchDialog extends Dialog {
};
/**
- * Listener for layout changes in the main layout. I use this to dynamically clean up
- * the layout of the dropdown and make it "pixel perfect."
- */
- private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener
- = new ViewTreeObserver.OnGlobalLayoutListener() {
-
- // It's very important that layoutSuggestionsList() does not reset
- // the values more than once, or this becomes an infinite loop.
- public void onGlobalLayout() {
- layoutSuggestionsList();
- }
- };
-
- /**
* Various ways to launch searches
*/
@@ -1333,7 +885,7 @@ public class SearchDialog extends Dialog {
* @param jamQuery True means to set the query, false means to reset it to the user's choice
*/
private void jamSuggestionQuery(boolean jamQuery, AdapterView<?> parent, int position) {
- mNonUserQuery = true; // disables any suggestions processing
+ mSuggestionsAdapter.setNonUserQuery(true); // disables any suggestions processing
if (jamQuery) {
CursorAdapter ca = getSuggestionsAdapter(parent);
Cursor c = ca.getCursor();
@@ -1356,7 +908,10 @@ public class SearchDialog extends Dialog {
}
if (jamText != null) {
mSearchTextField.setText(jamText);
- mSearchTextField.selectAll();
+ /* mSearchTextField.selectAll(); */ // this didn't work anyway in the old UI
+ // TODO this is only needed in the model where we have a selection in the ACTV
+ // and in the dropdown at the same time.
+ mSearchTextField.setSelection(jamText.length());
}
}
} else {
@@ -1372,7 +927,10 @@ public class SearchDialog extends Dialog {
mSearchTextField.selectAll();
}
}
- mNonUserQuery = false;
+ // TODO because the new query is (not) processed in another thread, we can't just
+ // take away this flag (yet). The better solution here is going to require a new API
+ // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
+// mSuggestionsAdapter.setNonUserQuery(false);
}
/**
@@ -1418,18 +976,6 @@ public class SearchDialog extends Dialog {
}
/**
- * Handler for clicks in the suggestions list
- */
- private OnItemClickListener mSuggestionsListItemClickListener = new OnItemClickListener() {
- public void onItemClick(AdapterView parent, View v, int position, long id) {
- // this guard protects against possible race conditions (late arrival of click)
- if (mSearchable != null) {
- launchSuggestion(parent, position);
- }
- }
- };
-
- /**
* Shared code for launching a query from a suggestion.
*
* @param av The AdapterView (really a ListView) containing the suggestions
@@ -1439,6 +985,16 @@ public class SearchDialog extends Dialog {
*/
private boolean launchSuggestion(AdapterView<?> av, int position) {
CursorAdapter ca = getSuggestionsAdapter(av);
+ return launchSuggestion(ca, position);
+ }
+
+ /**
+ * Shared code for launching a query from a suggestion.
+ * @param ca The cursor adapter containing the suggestions
+ * @param position The suggestion we'll be launching from
+ * @return true if a successful launch, false if could not (e.g. bad position)
+ */
+ private boolean launchSuggestion(CursorAdapter ca, int position) {
Cursor c = ca.getCursor();
if ((c != null) && c.moveToPosition(position)) {
setupSuggestionIntent(c, mSearchable);
@@ -1457,33 +1013,6 @@ public class SearchDialog extends Dialog {
}
/**
- * Manually adjust suggestions list into its perfectly-tweaked position.
- *
- * NOTE: This MUST not adjust the parameters if they are already set correctly,
- * or you create an infinite loop via the ViewTreeObserver.OnGlobalLayoutListener callback.
- */
- private void layoutSuggestionsList() {
- final int FUDGE_SUGG_X = 1;
- final int FUDGE_SUGG_WIDTH = 2;
-
- int[] itemLoc = new int[2];
- mSearchTextField.getLocationOnScreen(itemLoc);
- int x,width;
- x = itemLoc[0] + FUDGE_SUGG_X;
- width = mSearchTextField.getMeasuredWidth() + FUDGE_SUGG_WIDTH;
-
- // now set params and relayout
- ViewGroup.MarginLayoutParams lp;
- lp = (ViewGroup.MarginLayoutParams) mSuggestionsList.getLayoutParams();
- boolean changing = (lp.width != width) || (lp.leftMargin != x);
- if (changing) {
- lp.leftMargin = x;
- lp.width = width;
- mSuggestionsList.setLayoutParams(lp);
- }
- }
-
- /**
* 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
@@ -1585,6 +1114,301 @@ public class SearchDialog extends Dialog {
}
return result;
}
+
+ /**
+ * Support for AutoCompleteTextView-based suggestions
+ */
+ /**
+ * 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 {
+ private final String TAG = "SuggestionsAdapter";
+
+ SearchableInfo mSearchable;
+ private Resources mProviderResources;
+
+ // These private variables are shared by the filter thread and must be protected
+ private WeakReference<Cursor> mRecentCursor = new WeakReference<Cursor>(null);
+ private boolean mNonUserQuery = false;
+
+ public SuggestionsAdapter(Context context, SearchableInfo searchable) {
+ super(context, -1, null, null, null);
+ mSearchable = searchable;
+
+ // set up provider resources (gives us icons, etc.)
+ Context activityContext = mSearchable.getActivityContext(mContext);
+ Context providerContext = mSearchable.getProviderContext(mContext, activityContext);
+ mProviderResources = providerContext.getResources();
+ }
+
+ /**
+ * Set this field (temporarily!) to disable suggestions updating. This allows us
+ * to change the string in the text view without changing the suggestions list.
+ */
+ public void setNonUserQuery(boolean nonUserQuery) {
+ synchronized (this) {
+ mNonUserQuery = nonUserQuery;
+ }
+ }
+
+ public boolean getNonUserQuery() {
+ synchronized (this) {
+ return mNonUserQuery;
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * In order to provide the Search Mgr functionality of seeing your query change as you
+ * scroll through the list, we have to be able to jam new text into the string without
+ * retriggering the suggestions. We do that here via the "nonUserQuery" flag. In that
+ * case we simply return the existing cursor.
+ *
+ * TODO: Dianne suggests that this should simply be promoted into an AutoCompleteTextView
+ * behavior (perhaps optionally).
+ *
+ * TODO: The "nonuserquery" logic has a race condition because it happens in another thread.
+ * This also needs to be fixed.
+ */
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String query = (constraint == null) ? "" : constraint.toString();
+ Cursor c = null;
+ synchronized (this) {
+ if (mNonUserQuery) {
+ c = mRecentCursor.get();
+ mNonUserQuery = false;
+ }
+ }
+ if (c == null) {
+ c = getSuggestions(mSearchable, query);
+ synchronized (this) {
+ mRecentCursor = new WeakReference<Cursor>(c);
+ }
+ }
+ return c;
+ }
+
+ /**
+ * Overriding changeCursor() allows us to change not only the cursor, but by sampling
+ * the cursor's columns, the actual display characteristics of the list.
+ */
+ @Override
+ public void changeCursor(Cursor c) {
+
+ // first, check for various conditions that disqualify this cursor
+ if ((c == null) || (c.getCount() == 0)) {
+ // no cursor, or cursor with no data
+ changeCursorAndColumns(null, null, null);
+ if (c != null) {
+ c.close();
+ }
+ return;
+ }
+
+ // check cursor before trying to create list views from it
+ int colId = c.getColumnIndex("_id");
+ int col1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
+ int col2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
+ int colIc1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
+ int colIc2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
+
+ boolean minimal = (colId >= 0) && (col1 >= 0);
+ boolean hasIcons = (colIc1 >= 0) && (colIc2 >= 0);
+ boolean has2Lines = col2 >= 0;
+
+ if (minimal) {
+ int layout;
+ String[] from;
+ int[] to;
+
+ if (hasIcons) {
+ if (has2Lines) {
+ layout = com.android.internal.R.layout.search_dropdown_item_icons_2line;
+ from = TWO_LINE_ICONS_FROM;
+ to = TWO_LINE_ICONS_TO;
+ } else {
+ layout = com.android.internal.R.layout.search_dropdown_item_icons_1line;
+ from = ONE_LINE_ICONS_FROM;
+ to = ONE_LINE_ICONS_TO;
+ }
+ } else {
+ if (has2Lines) {
+ layout = com.android.internal.R.layout.search_dropdown_item_2line;
+ from = TWO_LINE_FROM;
+ to = TWO_LINE_TO;
+ } else {
+ layout = com.android.internal.R.layout.search_dropdown_item_1line;
+ from = ONE_LINE_FROM;
+ to = ONE_LINE_TO;
+ }
+ }
+ // Now actually set up the cursor, columns, and the list view
+ changeCursorAndColumns(c, from, to);
+ setViewResource(layout);
+ } else {
+ // Provide some help for developers instead of just silently discarding
+ Log.w(LOG_TAG, "Suggestions cursor discarded due to missing required columns.");
+ changeCursorAndColumns(null, null, null);
+ c.close();
+ }
+ if ((colIc1 >= 0) != (colIc2 >= 0)) {
+ Log.w(LOG_TAG, "Suggestion icon column(s) discarded, must be 0 or 2 columns.");
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ }
+
+ /**
+ * Implements OnItemClickListener
+ */
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+// Log.d(LOG_TAG, "onItemClick() position " + position);
+ launchSuggestion(mSuggestionsAdapter, position);
+ }
+
+ /**
+ * Implements OnItemSelectedListener
+ */
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+// Log.d(LOG_TAG, "onItemSelected() position " + position);
+ jamSuggestionQuery(true, parent, position);
+ }
+
+ /**
+ * Implements OnItemSelectedListener
+ */
+ public void onNothingSelected(AdapterView<?> parent) {
+// Log.d(LOG_TAG, "onNothingSelected()");
+ }
/**
* Debugging Support