summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKarl Rosaen <>2009-04-23 19:00:21 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2009-04-23 19:00:21 -0700
commit875d50a4b9294b2be33cff6493cae7acd1d07ea7 (patch)
tree48cc044c4719e53d214e5fa6c273d1ecd9078356
parentb08971b876801d9cb878f3f0ca0ebfde7c9bea8e (diff)
downloadframeworks_base-875d50a4b9294b2be33cff6493cae7acd1d07ea7.zip
frameworks_base-875d50a4b9294b2be33cff6493cae7acd1d07ea7.tar.gz
frameworks_base-875d50a4b9294b2be33cff6493cae7acd1d07ea7.tar.bz2
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
-rw-r--r--core/java/android/app/SearchDialog.java1514
-rw-r--r--core/java/android/app/SearchManager.java224
-rw-r--r--core/java/android/app/SuggestionsAdapter.java344
-rw-r--r--core/java/android/provider/Applications.java82
-rw-r--r--core/java/android/server/search/SearchManagerService.java49
-rw-r--r--core/java/android/server/search/SearchableInfo.java362
-rw-r--r--core/java/android/server/search/Searchables.java243
-rw-r--r--core/java/android/widget/AbsListView.java12
-rw-r--r--core/java/android/widget/AutoCompleteTextView.java248
-rw-r--r--core/res/res/drawable/btn_global_search.xml33
-rw-r--r--core/res/res/drawable/btn_global_search_normal.9.pngbin0 -> 676 bytes
-rw-r--r--core/res/res/drawable/btn_search_dialog.xml33
-rw-r--r--core/res/res/drawable/btn_search_dialog_default.9.pngbin0 -> 3203 bytes
-rw-r--r--core/res/res/drawable/btn_search_dialog_pressed.9.pngbin0 -> 3597 bytes
-rw-r--r--core/res/res/drawable/btn_search_dialog_selected.9.pngbin0 -> 3599 bytes
-rw-r--r--core/res/res/drawable/btn_search_dialog_voice.xml33
-rw-r--r--core/res/res/drawable/btn_search_dialog_voice_default.9.pngbin0 -> 3371 bytes
-rw-r--r--core/res/res/drawable/btn_search_dialog_voice_pressed.9.pngbin0 -> 3723 bytes
-rw-r--r--core/res/res/drawable/btn_search_dialog_voice_selected.9.pngbin0 -> 3691 bytes
-rwxr-xr-xcore/res/res/drawable/search_dropdown_background.9.pngbin0 -> 229 bytes
-rw-r--r--core/res/res/drawable/search_plate_global.9.pngbin0 -> 309 bytes
-rw-r--r--core/res/res/drawable/textfield_search.xml31
-rwxr-xr-xcore/res/res/drawable/textfield_search_default.9.pngbin0 -> 3361 bytes
-rw-r--r--core/res/res/drawable/textfield_search_pressed.9.pngbin0 -> 3586 bytes
-rwxr-xr-xcore/res/res/drawable/textfield_search_selected.9.pngbin0 -> 3354 bytes
-rw-r--r--core/res/res/layout/resolve_list_item.xml6
-rw-r--r--core/res/res/layout/search_bar.xml52
-rw-r--r--core/res/res/layout/search_dropdown_item_1line.xml4
-rw-r--r--core/res/res/layout/search_dropdown_item_2line.xml11
-rw-r--r--core/res/res/layout/search_dropdown_item_icons_1line.xml18
-rw-r--r--core/res/res/layout/search_dropdown_item_icons_2line.xml104
-rw-r--r--core/res/res/values/attrs.xml7
-rw-r--r--core/res/res/values/styles.xml19
-rw-r--r--core/res/res/values/themes.xml9
-rw-r--r--tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java374
-rw-r--r--tests/AndroidTests/src/com/android/unit_tests/SearchablesTest.java411
-rw-r--r--tests/FrameworkTest/tests/src/android/widget/AutoCompleteTextViewPopup.java52
37 files changed, 2610 insertions, 1665 deletions
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java
index a0cdb63..3d89ad7 100644
--- a/core/java/android/app/SearchDialog.java
+++ b/core/java/android/app/SearchDialog.java
@@ -16,24 +16,24 @@
package android.app;
+import static android.app.SuggestionsAdapter.getColumnString;
+
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.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.SystemClock;
import android.server.search.SearchableInfo;
import android.speech.RecognizerIntent;
@@ -45,7 +45,10 @@ import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
@@ -53,17 +56,14 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
-import android.widget.CursorAdapter;
import android.widget.ImageButton;
-import android.widget.ImageView;
import android.widget.ListView;
-import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
-import android.widget.WrapperListAdapter;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
-import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -75,59 +75,66 @@ import java.util.concurrent.atomic.AtomicLong;
public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener {
// Debugging support
- final static String LOG_TAG = "SearchDialog";
- private static final int DBG_LOG_TIMING = 0;
- final static int DBG_JAM_THREADING = 0;
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "SearchDialog";
+ private static final boolean DBG_LOG_TIMING = false;
- // interaction with runtime
- IntentFilter mCloseDialogsFilter;
- IntentFilter mPackageFilter;
-
private static final String INSTANCE_KEY_COMPONENT = "comp";
private static final String INSTANCE_KEY_APPDATA = "data";
private static final String INSTANCE_KEY_GLOBALSEARCH = "glob";
private static final String INSTANCE_KEY_DISPLAY_QUERY = "dQry";
private static final String INSTANCE_KEY_DISPLAY_SEL_START = "sel1";
private static final String INSTANCE_KEY_DISPLAY_SEL_END = "sel2";
- private static final String INSTANCE_KEY_USER_QUERY = "uQry";
- private static final String INSTANCE_KEY_SUGGESTION_QUERY = "sQry";
private static final String INSTANCE_KEY_SELECTED_ELEMENT = "slEl";
private static final int INSTANCE_SELECTED_BUTTON = -2;
private static final int INSTANCE_SELECTED_QUERY = -1;
-
+
+ // interaction with runtime
+ private IntentFilter mCloseDialogsFilter;
+ private IntentFilter mPackageFilter;
+
// views & widgets
private TextView mBadgeLabel;
- private AutoCompleteTextView mSearchTextField;
+ private SearchAutoComplete mSearchAutoComplete;
private Button mGoButton;
private ImageButton mVoiceButton;
+ private View mSearchPlate;
// interaction with searchable application
+ private SearchableInfo mSearchable;
private ComponentName mLaunchComponent;
private Bundle mAppSearchData;
private boolean mGlobalSearchMode;
private Context mActivityContext;
-
- // interaction with the search manager service
- private SearchableInfo mSearchable;
-
- // support for suggestions
- private String mUserQuery = null;
- private int mUserQuerySelStart;
- private int mUserQuerySelEnd;
- private boolean mLeaveJammedQueryOnRefocus = false;
- private String mPreviousSuggestionQuery = null;
- private int mPresetSelection = -1;
- private String mSuggestionAction = null;
- private Uri mSuggestionData = null;
- private String mSuggestionQuery = null;
+ // stack of previous searchables, to support the BACK key after
+ // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.
+ // The top of the stack (= previous searchable) is the last element of the list,
+ // since adding and removing is efficient at the end of an ArrayList.
+ private ArrayList<ComponentName> mPreviousComponents;
+
// For voice searching
private Intent mVoiceWebSearchIntent;
private Intent mVoiceAppSearchIntent;
// support for AutoCompleteTextView suggestions display
private SuggestionsAdapter mSuggestionsAdapter;
-
+
+ // Whether to rewrite queries when selecting suggestions
+ // TODO: This is disabled because of problems with persistent selections
+ // causing non-user-initiated rewrites.
+ private static final boolean REWRITE_QUERIES = false;
+
+ // The query entered by the user. This is not changed when selecting a suggestion
+ // that modifies the contents of the text field. But if the user then edits
+ // the suggestion, the resulting string is saved.
+ private String mUserQuery;
+
+ // 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> mOutsideDrawablesCache =
+ new WeakHashMap<String, Drawable>();
+
/**
* Constructor - fires it up and makes it look like the search UI.
*
@@ -153,25 +160,29 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
WindowManager.LayoutParams lp = theWindow.getAttributes();
- lp.setTitle("Search Dialog");
lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
theWindow.setAttributes(lp);
// get the view elements for local access
mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge);
- mSearchTextField = (AutoCompleteTextView)
+ mSearchAutoComplete = (SearchAutoComplete)
findViewById(com.android.internal.R.id.search_src_text);
mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn);
mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn);
+ mSearchPlate = findViewById(com.android.internal.R.id.search_plate);
// attach listeners
- mSearchTextField.addTextChangedListener(mTextWatcher);
- mSearchTextField.setOnKeyListener(mTextKeyListener);
+ mSearchAutoComplete.addTextChangedListener(mTextWatcher);
+ mSearchAutoComplete.setOnKeyListener(mTextKeyListener);
+ mSearchAutoComplete.setOnItemClickListener(this);
+ mSearchAutoComplete.setOnItemSelectedListener(this);
mGoButton.setOnClickListener(mGoButtonClickListener);
mGoButton.setOnKeyListener(mButtonsKeyListener);
mVoiceButton.setOnClickListener(mVoiceButtonClickListener);
mVoiceButton.setOnKeyListener(mButtonsKeyListener);
+ mSearchAutoComplete.setSearchDialog(this);
+
// pre-hide all the extraneous elements
mBadgeLabel.setVisibility(View.GONE);
@@ -199,7 +210,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
/**
* Set up the search dialog
*
- * @param Returns true if search dialog launched, false if not
+ * @return true if search dialog launched, false if not
*/
public boolean show(String initialQuery, boolean selectInitialQuery,
ComponentName componentName, Bundle appSearchData, boolean globalSearch) {
@@ -208,75 +219,65 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
// in this case, just discard the "show" request
return true;
}
-
- // Get searchable info from search manager and use to set up other elements of UI
- // Do this first so we can get out quickly if there's nothing to search
- ISearchManager sms;
- sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE));
- try {
- mSearchable = sms.getSearchableInfo(componentName, globalSearch);
- } catch (RemoteException e) {
- mSearchable = null;
+
+ // set up the searchable and show the dialog
+ if (!show(componentName, appSearchData, globalSearch)) {
+ return false;
+ }
+
+ // finally, load the user's initial text (which may trigger suggestions)
+ setUserQuery(initialQuery);
+ if (selectInitialQuery) {
+ mSearchAutoComplete.selectAll();
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets up the search dialog and shows it.
+ *
+ * @return <code>true</code> if search dialog launched
+ */
+ private boolean show(ComponentName componentName, Bundle appSearchData,
+ boolean globalSearch) {
+
+ if (DBG) {
+ Log.d(LOG_TAG, "show(" + componentName + ", "
+ + appSearchData + ", " + globalSearch + ")");
}
+
+ mSearchable = SearchManager.getSearchableInfo(componentName, globalSearch);
if (mSearchable == null) {
// unfortunately, we can't log here. it would be logspam every time the user
// clicks the "search" key on a non-search app
return false;
}
- // OK, we're going to show ourselves
- super.show();
-
- setupSearchableInfo();
-
mLaunchComponent = componentName;
mAppSearchData = appSearchData;
- mGlobalSearchMode = globalSearch;
-
- // receive broadcasts
- getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
- getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
+ // Using globalSearch here is just an optimization, just calling
+ // isDefaultSearchable() should always give the same result.
+ mGlobalSearchMode = globalSearch || SearchManager.isDefaultSearchable(mSearchable);
+ mActivityContext = mSearchable.getActivityContext(getContext());
- // configure the autocomplete aspects of the input box
- mSearchTextField.setOnItemClickListener(this);
- mSearchTextField.setOnItemSelectedListener(this);
-
- // This conversion is necessary to force a preload of the EditText and thus force
- // suggestions to be presented (even for an empty query)
- if (initialQuery == null) {
- initialQuery = ""; // This forces the preload to happen, triggering suggestions
+ // show the dialog. this will call onStart().
+ if (!isShowing()) {
+ show();
}
- // 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 = null;
- mSearchTextField.setAdapter(mSuggestionsAdapter);
- mSearchTextField.setText(initialQuery);
- } else {
- mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable,
- mSearchTextField);
- mSearchTextField.setAdapter(mSuggestionsAdapter);
-
- // finally, load the user's initial text (which may trigger suggestions)
- mSuggestionsAdapter.setNonUserQuery(false);
- mSearchTextField.setText(initialQuery);
- }
+ updateUI();
- if (selectInitialQuery) {
- mSearchTextField.selectAll();
- } else {
- mSearchTextField.setSelection(initialQuery.length());
- }
return true;
}
-
- /**
- * The default show() for this Dialog is not supported.
- */
+
@Override
- public void show() {
- return;
+ protected void onStart() {
+ super.onStart();
+
+ // receive broadcasts
+ getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
+ getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
}
/**
@@ -289,6 +290,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
public void onStop() {
super.onStop();
+ // TODO: Removing the listeners means that they never get called, since
+ // Dialog.dismissDialog() calls onStop() before sendDismissMessage().
setOnCancelListener(null);
setOnDismissListener(null);
@@ -299,26 +302,36 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
// This is OK - it just means we didn't have any registered
}
- // close any leftover cursor
- if (mSuggestionsAdapter != null) {
- mSuggestionsAdapter.changeCursor(null);
- }
+ closeSuggestionsAdapter();
// dump extra memory we're hanging on to
mLaunchComponent = null;
mAppSearchData = null;
mSearchable = null;
- mSuggestionAction = null;
- mSuggestionData = null;
- mSuggestionQuery = null;
mActivityContext = null;
- mPreviousSuggestionQuery = null;
mUserQuery = null;
+ mPreviousComponents = null;
+ }
+
+ /**
+ * Closes and gets rid of the suggestions adapter.
+ */
+ private void closeSuggestionsAdapter() {
+ // remove the adapter from the autocomplete first, to avoid any updates
+ // when we drop the cursor
+ mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
+ // close any leftover cursor
+ if (mSuggestionsAdapter != null) {
+ mSuggestionsAdapter.changeCursor(null);
+ }
+ mSuggestionsAdapter = null;
}
/**
* Save the minimal set of data necessary to recreate the search
*
+ * TODO: go through this and make sure that it saves everything that is needed
+ *
* @return A bundle with the state of the dialog.
*/
@Override
@@ -331,16 +344,14 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
// UI state
- bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchTextField.getText().toString());
- bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchTextField.getSelectionStart());
- bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchTextField.getSelectionEnd());
- bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
- bundle.putString(INSTANCE_KEY_SUGGESTION_QUERY, mPreviousSuggestionQuery);
+ bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchAutoComplete.getText().toString());
+ bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchAutoComplete.getSelectionStart());
+ bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchAutoComplete.getSelectionEnd());
int selectedElement = INSTANCE_SELECTED_QUERY;
if (mGoButton.isFocused()) {
selectedElement = INSTANCE_SELECTED_BUTTON;
- } else if (mSearchTextField.isPopupShowing()) {
+ } else if (mSearchAutoComplete.isPopupShowing()) {
selectedElement = 0; // TODO mSearchTextField.getListSelection() // 0..n
}
bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement);
@@ -350,6 +361,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
/**
* Restore the state of the dialog from a previously saved bundle.
+ *
+ * TODO: go through this and make sure that it saves everything that is saved
*
* @param savedInstanceState The state of the dialog previously saved by
* {@link #onSaveInstanceState()}.
@@ -365,26 +378,17 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
String displayQuery = savedInstanceState.getString(INSTANCE_KEY_DISPLAY_QUERY);
int querySelStart = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_START, -1);
int querySelEnd = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_END, -1);
- String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
int selectedElement = savedInstanceState.getInt(INSTANCE_KEY_SELECTED_ELEMENT);
- String suggestionQuery = savedInstanceState.getString(INSTANCE_KEY_SUGGESTION_QUERY);
// show the dialog. skip any show/hide animation, we want to go fast.
// send the text that actually generates the suggestions here; we'll replace the display
// text as necessary in a moment.
- if (!show(suggestionQuery, false, launchComponent, appSearchData, globalSearch)) {
+ if (!show(displayQuery, false, launchComponent, appSearchData, globalSearch)) {
// for some reason, we couldn't re-instantiate
return;
}
- if (mSuggestionsAdapter != null) {
- mSuggestionsAdapter.setNonUserQuery(true);
- }
- mSearchTextField.setText(displayQuery);
- // 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);
+ mSearchAutoComplete.setText(displayQuery);
// clean up the selection state
switch (selectedElement) {
@@ -395,21 +399,19 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
break;
case INSTANCE_SELECTED_QUERY:
if (querySelStart >= 0 && querySelEnd >= 0) {
- mSearchTextField.requestFocus();
- mSearchTextField.setSelection(querySelStart, querySelEnd);
+ mSearchAutoComplete.requestFocus();
+ mSearchAutoComplete.setSelection(querySelStart, querySelEnd);
}
break;
default:
- // defer selecting a list element until suggestion list appears
- mPresetSelection = selectedElement;
- // TODO mSearchTextField.setListSelection(selectedElement)
+ // TODO: defer selecting a list element until suggestion list appears
+// mSearchAutoComplete.setListSelection(selectedElement)
break;
}
}
/**
- * Hook for updating layout on a rotation
- *
+ * Called after resources have changed, e.g. after screen rotation or locale change.
*/
public void onConfigurationChanged(Configuration newConfig) {
if (isShowing()) {
@@ -419,15 +421,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
updateQueryHint();
}
}
-
+
/**
- * Use SearchableInfo record (from search manager service) to preconfigure the UI in various
- * ways.
+ * Update the UI according to the info in the current value of {@link #mSearchable}.
*/
- private void setupSearchableInfo() {
+ private void updateUI() {
if (mSearchable != null) {
- mActivityContext = mSearchable.getActivityContext(getContext());
-
+ updateSearchAutoComplete();
updateSearchButton();
updateSearchBadge();
updateQueryHint();
@@ -449,24 +449,38 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
}
}
- mSearchTextField.setInputType(inputType);
- mSearchTextField.setImeOptions(mSearchable.getImeOptions());
+ mSearchAutoComplete.setInputType(inputType);
+ mSearchAutoComplete.setImeOptions(mSearchable.getImeOptions());
}
}
-
+
/**
- * The list of installed packages has just changed. This means that our current context
- * may no longer be valid. This would only happen if a package is installed/removed exactly
- * when the search bar is open. So for now we're just going to close the search
- * bar.
- *
- * Anything fancier would require some checks to see if the user's context was still valid.
- * Which would be messier.
+ * Updates the auto-complete text view.
*/
- public void onPackageListChange() {
- cancel();
+ private void updateSearchAutoComplete() {
+ // close any existing suggestions adapter
+ closeSuggestionsAdapter();
+
+ mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
+ mSearchAutoComplete.setThreshold(0); // always allow zero-query suggestions
+
+ if (mGlobalSearchMode) {
+ mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in
+ mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
+ } else {
+ mSearchAutoComplete.setDropDownAlwaysVisible(false);
+ mSearchAutoComplete.setDropDownDismissedOnCompletion(true);
+ }
+
+ // 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(), mSearchable,
+ mOutsideDrawablesCache);
+ mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
+ }
}
-
+
/**
* Update the text in the search button. Note: This is deprecated functionality, for
* 1.0 compatibility only.
@@ -481,26 +495,40 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
iconLabel = getContext().getResources().
getDrawable(com.android.internal.R.drawable.ic_btn_search);
}
- mGoButton.setText(textLabel);
+ mGoButton.setText(textLabel);
mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
}
/**
- * Setup the search "Badge" if request by mode flags.
+ * Setup the search "Badge" if requested by mode flags.
*/
private void updateSearchBadge() {
// assume both hidden
int visibility = View.GONE;
Drawable icon = null;
- String text = null;
+ CharSequence text = null;
// optionally show one or the other.
if (mSearchable.mBadgeIcon) {
icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
visibility = View.VISIBLE;
+ if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
} else if (mSearchable.mBadgeLabel) {
text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
visibility = View.VISIBLE;
+ if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
+ } else if (!mGlobalSearchMode) {
+ // Get the localized name of the application which we are doing search in.
+ try {
+ PackageManager pm = getContext().getPackageManager();
+ ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0);
+ text = pm.getApplicationLabel(info.applicationInfo);
+ visibility = View.VISIBLE;
+ if (DBG) Log.d(LOG_TAG, "Using application label: " + text);
+ } catch (NameNotFoundException e) {
+ // app not found, fine, don't use its name for the label
+ Log.w(LOG_TAG, mLaunchComponent + " not found.");
+ }
}
mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
@@ -520,7 +548,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
hint = mActivityContext.getString(hintId);
}
}
- mSearchTextField.setHint(hint);
+ mSearchAutoComplete.setHint(hint);
}
}
@@ -548,63 +576,129 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
mVoiceButton.setVisibility(visibility);
}
+ /*
+ * Menu.
+ */
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Show search settings menu item if anyone handles the intent for it
+ Intent settingsIntent = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS);
+ PackageManager pm = getContext().getPackageManager();
+ ActivityInfo activityInfo = settingsIntent.resolveActivityInfo(pm, 0);
+ if (activityInfo != null) {
+ settingsIntent.setClassName(activityInfo.applicationInfo.packageName,
+ activityInfo.name);
+ String label = getActivityLabel(activityInfo);
+ menu.add(Menu.NONE, Menu.NONE, Menu.NONE, label)
+ .setIcon(android.R.drawable.ic_menu_preferences)
+ .setAlphabeticShortcut('P')
+ .setIntent(settingsIntent);
+ return true;
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ // TODO: shouldn't this be in PackageManager?
+ private String getActivityLabel(ActivityInfo activityInfo) {
+ PackageManager pm = getContext().getPackageManager();
+ try {
+ int labelRes = activityInfo.labelRes;
+ if (labelRes == 0) {
+ return null;
+ }
+ Resources r = pm.getResourcesForApplication(activityInfo.applicationInfo);
+ return r.getString(labelRes);
+ } catch (NameNotFoundException ex) {
+ return null;
+ }
+ }
+
/**
* Listeners of various types
*/
/**
+ * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
+ * touch is outside the window. But the window includes space for the drop-down,
+ * so we also cancel on taps outside the search bar when the drop-down is not showing.
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // cancel if the drop-down is not showing and the touch event was outside the search plate
+ if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
+ if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
+ cancel();
+ return true;
+ }
+ // Let Dialog handle events outside the window while the pop-up is showing.
+ return super.onTouchEvent(event);
+ }
+
+ private boolean isOutOfBounds(View v, MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
+ return (x < -slop) || (y < -slop)
+ || (x > (v.getWidth()+slop))
+ || (y > (v.getHeight()+slop));
+ }
+
+ /**
* Dialog's OnKeyListener implements various search-specific functionality
*
* @param keyCode This is the keycode of the typed key, and is the same value as
- * found in the KeyEvent parameter.
+ * found in the KeyEvent parameter.
* @param event The complete event record for the typed key
*
* @return Return true if the event was handled here, or false if not.
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- cancel();
+ if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
+
+ // handle back key to go back to previous searchable, etc.
+ if (handleBackKey(keyCode, event)) {
return true;
- case KeyEvent.KEYCODE_SEARCH:
- if (TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0) {
- launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ }
+
+ // search or cancel on search key
+ if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+ if (!mSearchAutoComplete.isEmpty()) {
+ launchQuerySearch();
} else {
cancel();
}
return true;
- default:
- SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
- if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
- launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
- return true;
- }
- break;
}
+
+ // 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.mQueryActionMsg != null)) {
+ launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
+ return true;
+ }
+
return false;
}
-
+
/**
* Callback to watch the textedit field for empty/non-empty
*/
private TextWatcher mTextWatcher = new TextWatcher() {
- public void beforeTextChanged(CharSequence s, int start, int
- before, int after) { }
+ public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
public void onTextChanged(CharSequence s, int start,
int before, int after) {
- if (DBG_LOG_TIMING == 1) {
+ if (DBG_LOG_TIMING) {
dbgLogTiming("onTextChanged()");
}
updateWidgetState();
- // Only do suggestions if actually typed by user
- if ((mSuggestionsAdapter != null) && !mSuggestionsAdapter.getNonUserQuery()) {
- mPreviousSuggestionQuery = s.toString();
- mUserQuery = mSearchTextField.getText().toString();
- mUserQuerySelStart = mSearchTextField.getSelectionStart();
- mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
+ if (!mSearchAutoComplete.isPerformingCompletion()) {
+ // The user changed the query, remember it.
+ mUserQuery = s == null ? "" : s.toString();
}
}
@@ -616,64 +710,34 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
private void updateWidgetState() {
// enable the button if we have one or more non-space characters
- boolean enabled =
- TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0;
-
+ boolean enabled = !mSearchAutoComplete.isEmpty();
mGoButton.setEnabled(enabled);
mGoButton.setFocusable(enabled);
}
- 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,
- SearchManager.SUGGEST_COLUMN_ICON_2};
- private final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_TEXT_2 };
- private final static String[] TWO_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_TEXT_2,
- SearchManager.SUGGEST_COLUMN_ICON_1,
- SearchManager.SUGGEST_COLUMN_ICON_2 };
-
- private final static int[] ONE_LINE_TO = {com.android.internal.R.id.text1};
- private final static int[] ONE_LINE_ICONS_TO = {com.android.internal.R.id.text1,
- com.android.internal.R.id.icon1,
- com.android.internal.R.id.icon2};
- private final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1,
- com.android.internal.R.id.text2};
- private final static int[] TWO_LINE_ICONS_TO = {com.android.internal.R.id.text1,
- com.android.internal.R.id.text2,
- com.android.internal.R.id.icon1,
- com.android.internal.R.id.icon2};
-
- /**
- * Safely retrieve the suggestions cursor adapter from the ListView
- *
- * @param adapterView The ListView containing our adapter
- * @result The CursorAdapter that we installed, or null if not set
- */
- private static CursorAdapter getSuggestionsAdapter(AdapterView<?> adapterView) {
- CursorAdapter result = null;
- if (adapterView != null) {
- Object ad = adapterView.getAdapter();
- if (ad instanceof CursorAdapter) {
- result = (CursorAdapter) ad;
- } else if (ad instanceof WrapperListAdapter) {
- result = (CursorAdapter) ((WrapperListAdapter)ad).getWrappedAdapter();
- }
- }
- return result;
- }
-
/**
* React to typing in the GO search button by refocusing to EditText.
* Continue typing the query.
*/
View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null) {
- return refocusingKeyListener(v, keyCode, event);
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return 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)) {
+ // restore focus and give key to EditText ...
+ if (mSearchAutoComplete.requestFocus()) {
+ return mSearchAutoComplete.dispatchKeyEvent(event);
+ }
}
+
return false;
}
};
@@ -683,10 +747,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
public void onClick(View v) {
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null) {
- launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return;
}
+ launchQuerySearch();
}
};
@@ -695,14 +760,16 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
public void onClick(View v) {
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return;
+ }
try {
if (mSearchable.getVoiceSearchLaunchWebSearch()) {
getContext().startActivity(mVoiceWebSearchIntent);
- dismiss();
} else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
getContext().startActivity(appSearchIntent);
- dismiss();
}
} catch (ActivityNotFoundException e) {
// Should not happen, since we check the availability of
@@ -778,136 +845,56 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- cancel();
- return true;
- }
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null &&
- TextUtils.getTrimmedLength(mSearchTextField.getText()) > 0) {
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("doTextKey()");
- }
- // dispatch "typing in the list" first
- if (mSearchTextField.isPopupShowing() &&
- mSearchTextField.getListSelection() != ListView.INVALID_POSITION) {
- return onSuggestionsKey(v, keyCode, event);
- }
- // otherwise, dispatch an "edit view" key
- switch (keyCode) {
- case KeyEvent.KEYCODE_ENTER:
- if (event.getAction() == KeyEvent.ACTION_UP) {
- v.cancelLongPress();
- launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
- return true;
- }
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- // capture the EditText state, so we can restore the user entry later
- mUserQuery = mSearchTextField.getText().toString();
- mUserQuerySelStart = mSearchTextField.getSelectionStart();
- mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
- // pass through - we're just watching here
- break;
- default:
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
- if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
- launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
- return true;
- }
- }
- break;
- }
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return false;
}
- return false;
- }
- };
-
- /**
- * 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).
- */
- private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
- boolean handled = false;
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null) {
- handled = doSuggestionsKey(v, keyCode, event);
- }
- return handled;
- }
-
- /**
- * Per UI design, we're going to "steer" any typed keystrokes back into the EditText
- * box, even if the user has navigated the focus to the dropdown or to the GO button.
- *
- * @param v The view into which the keystroke was typed
- * @param keyCode keyCode of entered key
- * @param event Full KeyEvent record of entered key
- */
- private boolean refocusingKeyListener(View v, int keyCode, KeyEvent event) {
- 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)) {
- // restore focus and give key to EditText ...
- // but don't replace the user's query
- mLeaveJammedQueryOnRefocus = true;
- if (mSearchTextField.requestFocus()) {
- handled = mSearchTextField.dispatchKeyEvent(event);
+ if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
+ if (DBG) {
+ Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
+ + "), selection: " + mSearchAutoComplete.getListSelection());
}
- mLeaveJammedQueryOnRefocus = false;
- }
- return handled;
- }
-
- /**
- * 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)
- if (mSearchable == null) {
- return;
+
+ // If a suggestion is selected, handle enter, search key, and action keys
+ // as presses on the selected suggestion
+ if (mSearchAutoComplete.isPopupShowing() &&
+ mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
+ return onSuggestionsKey(v, keyCode, event);
}
- // Update query text based on navigation in to/out of the suggestions list
- if (hasFocus) {
- // Entering the list view - record selection point from user's query
- mUserQuery = mSearchTextField.getText().toString();
- mUserQuerySelStart = mSearchTextField.getSelectionStart();
- mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
- // then update the query to match the entered selection
- jamSuggestionQuery(true, mSuggestionsList,
- mSuggestionsList.getSelectedItemPosition());
- } else {
- // Exiting the list view
-
- if (mSuggestionsList.getSelectedItemPosition() < 0) {
- // Direct exit - Leave new suggestion in place (do nothing)
- } else {
- // Navigation exit - restore user's query text
- if (!mLeaveJammedQueryOnRefocus) {
- jamSuggestionQuery(false, null, -1);
+
+ // If there is text in the query box, handle enter, and action keys
+ // The search key is handled by the dialog's onKeyDown().
+ if (!mSearchAutoComplete.isEmpty()) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ v.cancelLongPress();
+ launchQuerySearch();
+ return true;
+ }
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
+ launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
+ return true;
}
}
}
-
+ return false;
}
};
- */
-
+
/**
- * 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
+ * When the ACTION_CLOSE_SYSTEM_DIALOGS intent is received, we should close ourselves
+ * immediately, in order to allow a higher-priority UI to take over
* (e.g. phone call received).
+ *
+ * When a package is added, removed or changed, our current context
+ * may no longer be valid. This would only happen if a package is installed/removed exactly
+ * when the search bar is open. So for now we're just going to close the search
+ * bar.
+ * Anything fancier would require some checks to see if the user's context was still valid.
+ * Which would be messier.
*/
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -918,7 +905,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)
|| Intent.ACTION_PACKAGE_REMOVED.equals(action)
|| Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
- onPackageListChange();
+ cancel();
}
}
};
@@ -938,58 +925,45 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
/**
- * Various ways to launch searches
+ * React to the user typing while in the suggestions list. First, check for action
+ * keys. If not handled, try refocusing regular characters into the EditText.
*/
-
- /**
- * React to the user clicking the "GO" button. Hide the UI and launch a search.
- *
- * @param actionKey Pass a keycode if the launch was triggered by an action key. Pass
- * KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
- * @param actionMsg Pass the suggestion-provided message if the launch was triggered by an
- * action key. Pass null for no actionKey message.
- */
- private void launchQuerySearch(int actionKey, final String actionMsg) {
- final String query = mSearchTextField.getText().toString();
- final Bundle appData = mAppSearchData;
- final SearchableInfo si = mSearchable; // cache briefly (dismiss() nulls it)
- dismiss();
- sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, actionKey, actionMsg, si);
- }
-
- /**
- * React to the user typing an action key while in the suggestions list
- */
- private boolean doSuggestionsKey(View v, int keyCode, KeyEvent event) {
- // Exit early in case of race condition
+ private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
+ // guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable == null) {
+ return false;
+ }
if (mSuggestionsAdapter == null) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("doSuggestionsKey()");
+ if (DBG_LOG_TIMING) {
+ dbgLogTiming("onSuggestionsKey()");
}
// First, check for enter or search (both of which we'll treat as a "click")
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
- int position = mSearchTextField.getListSelection();
- return launchSuggestion(mSuggestionsAdapter, position);
+ int position = mSearchAutoComplete.getListSelection();
+ return launchSuggestion(position);
}
// Next, check for left/right moves, which we use to "return" the user to the edit view
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
- // give "focus" to text editor, but don't restore the user's original query
+ // give "focus" to text editor, with cursor at the beginning if
+ // left key, at end if right key
+ // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
- 0 : mSearchTextField.length();
- mSearchTextField.setSelection(selPoint);
- mSearchTextField.setListSelection(0);
- mSearchTextField.clearListSelection();
+ 0 : mSearchAutoComplete.length();
+ mSearchAutoComplete.setSelection(selPoint);
+ mSearchAutoComplete.setListSelection(0);
+ mSearchAutoComplete.clearListSelection();
return true;
}
// Next, check for an "up and out" move
- if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchTextField.getListSelection()) {
- jamSuggestionQuery(false, null, -1);
+ if (keyCode == KeyEvent.KEYCODE_DPAD_UP
+ && 0 == mSearchAutoComplete.getListSelection()) {
+ restoreUserQuery();
// let ACTV complete the move
return false;
}
@@ -999,158 +973,194 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
if ((actionKey != null) &&
((actionKey.mSuggestActionMsg != null) ||
(actionKey.mSuggestActionMsgColumn != null))) {
- // launch suggestion using action key column
- int position = mSearchTextField.getListSelection();
- if (position >= 0) {
+ // launch suggestion using action key column
+ int position = mSearchAutoComplete.getListSelection();
+ if (position != ListView.INVALID_POSITION) {
Cursor c = mSuggestionsAdapter.getCursor();
if (c.moveToPosition(position)) {
final String actionMsg = getActionKeyMessage(c, actionKey);
if (actionMsg != null && (actionMsg.length() > 0)) {
- // shut down search bar and launch the activity
- // cache everything we need because dismiss releases mems
- setupSuggestionIntent(c, mSearchable);
- final String query = mSearchTextField.getText().toString();
- final Bundle appData = mAppSearchData;
- SearchableInfo si = mSearchable;
- String suggestionAction = mSuggestionAction;
- Uri suggestionData = mSuggestionData;
- String suggestionQuery = mSuggestionQuery;
- dismiss();
- sendLaunchIntent(suggestionAction, suggestionData,
- suggestionQuery, appData,
- keyCode, actionMsg, si);
- return true;
+ return launchSuggestion(position, keyCode, actionMsg);
}
}
}
}
}
return false;
- }
+ }
+
+ /**
+ * Launch a search for the text in the query text field.
+ */
+ protected void launchQuerySearch() {
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ }
/**
- * Set or reset the user query to follow the selections in the suggestions
+ * Launch a search for the text in the query text field.
+ *
+ * @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.
+ */
+ protected void launchQuerySearch(int actionKey, String actionMsg) {
+ String query = mSearchAutoComplete.getText().toString();
+ Intent intent = createIntent(Intent.ACTION_SEARCH, null, query, null,
+ actionKey, actionMsg);
+ launchIntent(intent);
+ }
+
+ /**
+ * Launches an intent based on a suggestion.
*
- * @param jamQuery True means to set the query, false means to reset it to the user's choice
+ * @param position The index of the suggestion to create the intent from.
+ * @return true if a successful launch, false if could not (e.g. bad position).
*/
- private void jamSuggestionQuery(boolean jamQuery, AdapterView<?> parent, int position) {
- // quick check against race conditions
- if (mSearchable == null) {
+ protected boolean launchSuggestion(int position) {
+ return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
+ }
+
+ /**
+ * 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).
+ */
+ protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
+ Cursor c = mSuggestionsAdapter.getCursor();
+ if ((c != null) && c.moveToPosition(position)) {
+ Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
+ launchIntent(intent);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Launches an intent. Also dismisses the search dialog if not in global search mode.
+ */
+ private void launchIntent(Intent intent) {
+ if (intent == null) {
return;
}
-
- mSuggestionsAdapter.setNonUserQuery(true); // disables any suggestions processing
- if (jamQuery) {
- CursorAdapter ca = getSuggestionsAdapter(parent);
- Cursor c = ca.getCursor();
- if (c.moveToPosition(position)) {
- setupSuggestionIntent(c, mSearchable);
- String jamText = null;
-
- // Simple heuristic for selecting text with which to rewrite the query.
- if (mSuggestionQuery != null) {
- jamText = mSuggestionQuery;
- } else if (mSearchable.mQueryRewriteFromData && (mSuggestionData != null)) {
- jamText = mSuggestionData.toString();
- } else if (mSearchable.mQueryRewriteFromText) {
- try {
- int column = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1);
- jamText = c.getString(column);
- } catch (RuntimeException e) {
- // no work here, jamText is null
- }
- }
- if (jamText != null) {
- mSearchTextField.setText(jamText);
- /* 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 {
- // reset user query
- mSearchTextField.setText(mUserQuery);
- try {
- mSearchTextField.setSelection(mUserQuerySelStart, mUserQuerySelEnd);
- } catch (IndexOutOfBoundsException e) {
- // In case of error, just select all
- Log.e(LOG_TAG, "Caught IndexOutOfBoundsException while setting selection. " +
- "start=" + mUserQuerySelStart + " end=" + mUserQuerySelEnd +
- " text=\"" + mUserQuery + "\"");
- mSearchTextField.selectAll();
- }
+ if (handleSpecialIntent(intent)){
+ return;
+ }
+ if (!mGlobalSearchMode) {
+ dismiss();
}
- // 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);
+ getContext().startActivity(intent);
}
-
+
/**
- * Assemble a search intent and send it.
- *
- * @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.
+ * Handles the special intent actions declared in {@link SearchManager}.
+ *
+ * @return <code>true</code> if the intent was handled.
*/
- 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);
-
- if (query != null) {
- launcher.putExtra(SearchManager.QUERY, query);
+ private boolean handleSpecialIntent(Intent intent) {
+ String action = intent.getAction();
+ if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
+ handleChangeSourceIntent(intent);
+ return true;
+ } else if (SearchManager.INTENT_ACTION_CURSOR_RESPOND.equals(action)) {
+ handleCursorRespondIntent(intent);
+ return true;
}
-
- if (data != null) {
- launcher.setData(data);
+ return false;
+ }
+
+ /**
+ * Handles SearchManager#INTENT_ACTION_CHANGE_SOURCE.
+ */
+ private void handleChangeSourceIntent(Intent intent) {
+ Uri dataUri = intent.getData();
+ if (dataUri == null) {
+ Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
+ return;
}
-
- if (appData != null) {
- launcher.putExtra(SearchManager.APP_DATA, appData);
+ ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
+ if (componentName == null) {
+ Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
+ return;
}
-
- // add launch info (action key, etc.)
- if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
- launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
- launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
+ if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);
+
+ ComponentName previous = mLaunchComponent;
+ if (!show(componentName, mAppSearchData, false)) {
+ Log.w(LOG_TAG, "Failed to switch to source " + componentName);
+ return;
}
+ pushPreviousComponent(previous);
- // attempt to enforce security requirement (no 3rd-party intents)
- launcher.setComponent(si.mSearchActivity);
-
- getContext().startActivity(launcher);
+ String query = intent.getStringExtra(SearchManager.QUERY);
+ setUserQuery(query);
}
-
+
/**
- * 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)
+ * Handles {@link SearchManager#INTENT_ACTION_CURSOR_RESPOND}.
*/
- private boolean launchSuggestion(CursorAdapter ca, int position) {
- Cursor c = ca.getCursor();
- if ((c != null) && c.moveToPosition(position)) {
- setupSuggestionIntent(c, mSearchable);
-
- final Bundle appData = mAppSearchData;
- SearchableInfo si = mSearchable;
- String suggestionAction = mSuggestionAction;
- Uri suggestionData = mSuggestionData;
- String suggestionQuery = mSuggestionQuery;
- dismiss();
- sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, appData,
- KeyEvent.KEYCODE_UNKNOWN, null, si);
- return true;
+ private void handleCursorRespondIntent(Intent intent) {
+ Cursor c = mSuggestionsAdapter.getCursor();
+ if (c != null) {
+ c.respond(intent.getExtras());
}
- return false;
+ }
+
+ /**
+ * Saves the previous component that was searched, so that we can go
+ * back to it.
+ */
+ private void pushPreviousComponent(ComponentName componentName) {
+ if (mPreviousComponents == null) {
+ mPreviousComponents = new ArrayList<ComponentName>();
+ }
+ mPreviousComponents.add(componentName);
+ }
+
+ /**
+ * Pops the previous component off the stack and returns it.
+ *
+ * @return The component name, or <code>null</code> if there was
+ * no previous component.
+ */
+ private ComponentName popPreviousComponent() {
+ if (mPreviousComponents == null) {
+ return null;
+ }
+ int size = mPreviousComponents.size();
+ if (size == 0) {
+ return null;
+ }
+ return mPreviousComponents.remove(size - 1);
+ }
+
+ /**
+ * Goes back to the previous component that was searched, if any.
+ *
+ * @return <code>true</code> if there was a previous component that we could go back to.
+ */
+ private boolean backToPreviousComponent() {
+ ComponentName previous = popPreviousComponent();
+ if (previous == null) {
+ return false;
+ }
+ if (!show(previous, mAppSearchData, false)) {
+ Log.w(LOG_TAG, "Failed to switch to source " + previous);
+ return false;
+ }
+
+ // must touch text to trigger suggestions
+ // TODO: should this be the text as it was when the user left
+ // the source that we are now going back to?
+ String query = mSearchAutoComplete.getText().toString();
+ setUserQuery(query);
+
+ return true;
}
/**
@@ -1159,62 +1169,43 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
* 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.
- *
* @param c The suggestions cursor, moved to the row of the user's selection
- * @param si The searchable activity's info record
+ * @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.
*/
- void setupSuggestionIntent(Cursor c, SearchableInfo si) {
+ private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
try {
// use specific action if supplied, or default action if supplied, or fixed default
- mSuggestionAction = null;
- int mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
- if (mColumn >= 0) {
- final String action = c.getString(mColumn);
- if (action != null) {
- mSuggestionAction = action;
- }
- }
- if (mSuggestionAction == null) {
- mSuggestionAction = si.getSuggestIntentAction();
+ String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
+ if (action == null) {
+ action = mSearchable.getSuggestIntentAction();
}
- if (mSuggestionAction == null) {
- mSuggestionAction = Intent.ACTION_SEARCH;
+ if (action == null) {
+ action = Intent.ACTION_SEARCH;
}
// use specific data if supplied, or default data if supplied
- String data = null;
- mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
- if (mColumn >= 0) {
- final String rowData = c.getString(mColumn);
- if (rowData != null) {
- data = rowData;
- }
- }
+ String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
if (data == null) {
- data = si.getSuggestIntentData();
+ data = mSearchable.getSuggestIntentData();
}
-
// then, if an ID was provided, append it.
if (data != null) {
- mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
- if (mColumn >= 0) {
- final String id = c.getString(mColumn);
- if (id != null) {
- data = data + "/" + Uri.encode(id);
- }
+ String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
+ if (id != null) {
+ data = data + "/" + Uri.encode(id);
}
}
- mSuggestionData = (data == null) ? null : Uri.parse(data);
+ Uri dataUri = (data == null) ? null : Uri.parse(data);
+
+ String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
- mSuggestionQuery = null;
- mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
- if (mColumn >= 0) {
- final String query = c.getString(mColumn);
- if (query != null) {
- mSuggestionQuery = query;
- }
- }
+ String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
+
+ return createIntent(action, dataUri, query, extraData, actionKey, actionMsg);
} catch (RuntimeException e ) {
int rowNum;
try { // be really paranoid now
@@ -1224,10 +1215,49 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
" returned exception" + e.toString());
+ return null;
}
}
/**
+ * 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 query Intent query, or <code>null</code>.
+ * @param extraData Data for {@link SearchManager#EXTRA_DATA_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.
+ * @return The intent.
+ */
+ private Intent createIntent(String action, Uri data, String query, String extraData,
+ int actionKey, String actionMsg) {
+ // Now build the Intent
+ Intent intent = new Intent(action);
+ if (data != null) {
+ intent.setData(data);
+ }
+ if (query != null) {
+ intent.putExtra(SearchManager.QUERY, query);
+ }
+ if (extraData != null) {
+ intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+ }
+ if (mAppSearchData != null) {
+ intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
+ }
+ if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
+ intent.putExtra(SearchManager.ACTION_KEY, actionKey);
+ intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
+ }
+ // attempt to enforce security requirement (no 3rd-party intents)
+ intent.setComponent(mSearchable.mSearchActivity);
+ return intent;
+ }
+
+ /**
* For a given suggestion and a given cursor row, get the action message. If not provided
* by the specific row/column, also check for a single definition (for the action key).
*
@@ -1236,17 +1266,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*
* @return Returns a string, or null if no action key message for this suggestion
*/
- private String getActionKeyMessage(Cursor c, final SearchableInfo.ActionKeyInfo actionKey) {
+ private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
String result = null;
// check first in the cursor data, for a suggestion-specific message
final String column = actionKey.mSuggestActionMsgColumn;
if (column != null) {
- try {
- int colId = c.getColumnIndexOrThrow(column);
- result = c.getString(colId);
- } catch (RuntimeException e) {
- // OK - result is already null
- }
+ result = SuggestionsAdapter.getColumnString(c, column);
}
// If the cursor didn't give us a message, see if there's a single message defined
// for the actionkey (for all suggestions)
@@ -1257,343 +1282,178 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
/**
- * Local subclass for AutoCompleteTextView
- *
- * This exists entirely to override the threshold method. Otherwise we just use the class
- * as-is.
+ * Local subclass for AutoCompleteTextView.
*/
public static class SearchAutoComplete extends AutoCompleteTextView {
+ private int mThreshold;
+ private SearchDialog mSearchDialog;
+
public SearchAutoComplete(Context context) {
super(null);
+ mThreshold = getThreshold();
}
public SearchAutoComplete(Context context, AttributeSet attrs) {
super(context, attrs);
+ mThreshold = getThreshold();
}
public SearchAutoComplete(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mThreshold = getThreshold();
}
-
- /**
- * We never allow ACTV to automatically replace the text, since we use "jamSuggestionQuery"
- * to do that. There's no point in letting ACTV do this here, because in the search UI,
- * as soon as we click a suggestion, we're going to start shutting things down.
- */
- @Override
- public void replaceText(CharSequence text) {
+
+ private void setSearchDialog(SearchDialog searchDialog) {
+ mSearchDialog = searchDialog;
}
- /**
- * We always return true, so that the effective threshold is "zero". This allows us
- * to provide "null" suggestions such as "just show me some recent entries".
- */
@Override
- public boolean enoughToFilter() {
- return true;
+ public void setThreshold(int threshold) {
+ super.setThreshold(threshold);
+ mThreshold = threshold;
}
- }
-
- /**
- * 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;
- private AutoCompleteTextView mParentView;
- public SuggestionsAdapter(Context context, SearchableInfo searchable,
- AutoCompleteTextView actv) {
- super(context, -1, null, null, null);
- mSearchable = searchable;
- mParentView = actv;
-
- // 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.
+ * Returns true if the text field is empty, or contains only whitespace.
*/
- public void setNonUserQuery(boolean nonUserQuery) {
- synchronized (this) {
- mNonUserQuery = nonUserQuery;
- }
+ private boolean isEmpty() {
+ return TextUtils.getTrimmedLength(getText()) == 0;
}
- 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.
+ * Clears the entered text.
*/
- @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;
+ private void clear() {
+ setText("");
}
/**
- * Overriding changeCursor() allows us to change not only the cursor, but by sampling
- * the cursor's columns, the actual display characteristics of the list.
+ * We override this method to avoid replacing the query box text
+ * when a suggestion is clicked.
*/
@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;
- }
- }
- // Force the underlying ListView to discard and reload all layouts
- // (Note, this should be optimized for cases where layout/cursor remain same)
- mParentView.resetListAndClearViews();
- // 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.");
- }
+ protected void replaceText(CharSequence text) {
}
/**
- * 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.
+ * We override this method so that we can allow a threshold of zero, which ACTV does not.
*/
@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;
+ public boolean enoughToFilter() {
+ return mThreshold <= 0 || super.enoughToFilter();
}
-
+
/**
- * 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
+ * {@link AutoCompleteTextView#onKeyPreIme(int, KeyEvent)}) dismisses the drop-down on BACK,
+ * so we must override this method to modify the BACK behavior.
*/
@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);
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return mSearchDialog.handleBackKey(keyCode, event);
}
-
- /**
- * 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;
+ }
+
+ protected boolean handleBackKey(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
+ mSearchAutoComplete.dismissDropDown();
+ if (backToPreviousComponent()) {
+ return true;
}
+ if (!mSearchAutoComplete.isEmpty()) {
+ mSearchAutoComplete.clear();
+ return true;
+ }
+ cancel();
+ return true;
}
-
+ return false;
}
/**
* Implements OnItemClickListener
*/
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- // Log.d(LOG_TAG, "onItemClick() position " + position);
- launchSuggestion(mSuggestionsAdapter, position);
+ if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
+ launchSuggestion(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);
+ 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.
+ if (REWRITE_QUERIES) {
+ rewriteQueryFromSuggestion(position);
+ }
}
/**
* Implements OnItemSelectedListener
*/
public void onNothingSelected(AdapterView<?> parent) {
- // Log.d(LOG_TAG, "onNothingSelected()");
+ if (DBG) Log.d(LOG_TAG, "onNothingSelected()");
+ }
+
+ /**
+ * Query rewriting.
+ */
+
+ private void rewriteQueryFromSuggestion(int position) {
+ 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.
+ if (DBG) Log.d(LOG_TAG, "Rewriting query to '" + newQuery + "'");
+ // Update the text field, without getting new suggestions.
+ setQuery(newQuery);
+ } else {
+ // The suggestion does not rewrite the query, restore the user's query.
+ if (DBG) Log.d(LOG_TAG, "Suggestion gives no rewrite, restoring user query.");
+ restoreUserQuery();
+ }
+ } else {
+ // We got a bad position, restore the user's query.
+ Log.w(LOG_TAG, "Bad suggestion position: " + position);
+ restoreUserQuery();
+ }
+ }
+
+ /**
+ * Restores the query entered by the user if needed.
+ */
+ private void restoreUserQuery() {
+ if (DBG) Log.d(LOG_TAG, "Restoring query to '" + mUserQuery + "'");
+ setQuery(mUserQuery);
+ }
+
+ /**
+ * Sets the text in the query box, without updating the suggestions.
+ */
+ private void setQuery(CharSequence query) {
+ mSearchAutoComplete.setText(query, false);
+ if (query != null) {
+ mSearchAutoComplete.setSelection(query.length());
+ }
+ }
+
+ /**
+ * Sets the text in the query box, updating the suggestions.
+ */
+ private void setUserQuery(String query) {
+ if (query == null) {
+ query = "";
+ }
+ mUserQuery = query;
+ mSearchAutoComplete.setText(query);
+ mSearchAutoComplete.setSelection(query.length());
}
/**
diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java
index ecdd3f8..3e9f3dd 100644
--- a/core/java/android/app/SearchManager.java
+++ b/core/java/android/app/SearchManager.java
@@ -17,12 +17,17 @@
package android.app;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
+import android.database.Cursor;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
+import android.os.RemoteException;
import android.os.ServiceManager;
+import android.server.search.SearchableInfo;
import android.view.KeyEvent;
/**
@@ -439,20 +444,18 @@ import android.view.KeyEvent;
*
* <tr><th>{@link #SUGGEST_COLUMN_ICON_1}</th>
* <td>If your cursor includes this column, then all suggestions will be provided in an
- * icons+text format. This value should be a reference (resource ID) of the icon to
+ * icons+text format. This value should be a reference to the icon to
* draw on the left side, or it can be null or zero to indicate no icon in this row.
- * You must provide both cursor columns, or neither.
* </td>
- * <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_2}</td>
+ * <td align="center">No.</td>
* </tr>
*
* <tr><th>{@link #SUGGEST_COLUMN_ICON_2}</th>
* <td>If your cursor includes this column, then all suggestions will be provided in an
- * icons+text format. This value should be a reference (resource ID) of the icon to
+ * icons+text format. This value should be a reference to the icon to
* draw on the right side, or it can be null or zero to indicate no icon in this row.
- * You must provide both cursor columns, or neither.
* </td>
- * <td align="center">No, but required if you also have {@link #SUGGEST_COLUMN_ICON_1}</td>
+ * <td align="center">No.</td>
* </tr>
*
* <tr><th>{@link #SUGGEST_COLUMN_INTENT_ACTION}</th>
@@ -1155,6 +1158,13 @@ public class SearchManager
public final static String ACTION_KEY = "action_key";
/**
+ * Intent extra data key: This key will be used for the extra populated by the
+ * {@link #SUGGEST_COLUMN_INTENT_EXTRA_DATA} column.
+ * {@hide}
+ */
+ public final static String EXTRA_DATA_KEY = "intent_extra_data_key";
+
+ /**
* Intent extra data key: Use this key with Intent.ACTION_SEARCH and
* {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()}
* to obtain the action message that was defined for a particular search action key and/or
@@ -1195,21 +1205,59 @@ public class SearchManager
public final static String SUGGEST_COLUMN_TEXT_2 = "suggest_text_2";
/**
* Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
- * then all suggestions will be provided in format that includes space for two small icons,
+ * then all suggestions will be provided in a format that includes space for two small icons,
* one at the left and one at the right of each suggestion. The data in the column must
- * be a a resource ID for the icon you wish to have displayed. If you include this column,
- * you must also include {@link #SUGGEST_COLUMN_ICON_2}.
+ * be a resource ID of a drawable, or a URI in one of the following formats:
+ *
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ *
+ * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)}
+ * for more information on these schemes.
*/
public final static String SUGGEST_COLUMN_ICON_1 = "suggest_icon_1";
/**
* Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
- * then all suggestions will be provided in format that includes space for two small icons,
+ * then all suggestions will be provided in a format that includes space for two small icons,
* one at the left and one at the right of each suggestion. The data in the column must
- * be a a resource ID for the icon you wish to have displayed. If you include this column,
- * you must also include {@link #SUGGEST_COLUMN_ICON_1}.
+ * be a resource ID of a drawable, or a URI in one of the following formats:
+ *
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ *
+ * See {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)}
+ * for more information on these schemes.
*/
public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2";
/**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in a format that includes space for two small icons,
+ * one at the left and one at the right of each suggestion. The data in the column must
+ * be a blob that contains a bitmap.
+ *
+ * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_1} column.
+ *
+ * @hide
+ */
+ public final static String SUGGEST_COLUMN_ICON_1_BITMAP = "suggest_icon_1_bitmap";
+ /**
+ * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column,
+ * then all suggestions will be provided in a format that includes space for two small icons,
+ * one at the left and one at the right of each suggestion. The data in the column must
+ * be a blob that contains a bitmap.
+ *
+ * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_2} column.
+ *
+ * @hide
+ */
+ public final static String SUGGEST_COLUMN_ICON_2_BITMAP = "suggest_icon_2_bitmap";
+ /**
* Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
* this element exists at the given row, this is the action that will be used when
* forming the suggestion's intent. If the element is not provided, the action will be taken
@@ -1230,6 +1278,14 @@ public class SearchManager
*/
public final static String SUGGEST_COLUMN_INTENT_DATA = "suggest_intent_data";
/**
+ * Column name for suggestions cursor. <i>Optional.</i> This column allows suggestions
+ * to provide additional arbitrary data which will be included as an extra under the key
+ * {@link #EXTRA_DATA_KEY}.
+ *
+ * @hide pending API council approval
+ */
+ public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data";
+ /**
* Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i>
* this element exists at the given row, then "/" and this value will be appended to the data
* field in the Intent. This should only be used if the data field has already been set to an
@@ -1244,6 +1300,54 @@ public class SearchManager
*/
public final static String SUGGEST_COLUMN_QUERY = "suggest_intent_query";
+ /**
+ * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION},
+ * the search dialog will switch to a different suggestion source when the
+ * suggestion is clicked.
+ *
+ * {@link #SUGGEST_COLUMN_INTENT_DATA} must contain
+ * the flattened {@link ComponentName} of the activity which is to be searched.
+ *
+ * TODO: Should {@link #SUGGEST_COLUMN_INTENT_DATA} instead contain a URI in the format
+ * used by {@link android.provider.Applications}?
+ *
+ * TODO: This intent should be protected by the same permission that we use
+ * for replacing the global search provider.
+ *
+ * The query text field will be set to the value of {@link #SUGGEST_COLUMN_QUERY}.
+ *
+ * @hide Pending API council approval.
+ */
+ public final static String INTENT_ACTION_CHANGE_SEARCH_SOURCE
+ = "android.search.action.CHANGE_SEARCH_SOURCE";
+
+ /**
+ * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION},
+ * the search dialog will call {@link Cursor#respond(Bundle)} when the
+ * suggestion is clicked.
+ *
+ * The {@link Bundle} argument will be constructed
+ * in the same way as the "extra" bundle included in an Intent constructed
+ * from the suggestion.
+ *
+ * @hide Pending API council approval.
+ */
+ public final static String INTENT_ACTION_CURSOR_RESPOND
+ = "android.search.action.CURSOR_RESPOND";
+
+ /**
+ * Intent action for starting the global search settings activity.
+ * The global search provider should handle this intent.
+ *
+ * @hide Pending API council approval.
+ */
+ public final static String INTENT_ACTION_SEARCH_SETTINGS
+ = "android.search.action.SEARCH_SETTINGS";
+
+ /**
+ * Reference to the shared system search service.
+ */
+ private static ISearchManager sService = getSearchManagerService();
private final Context mContext;
private final Handler mHandler;
@@ -1257,12 +1361,6 @@ public class SearchManager
mContext = context;
mHandler = handler;
}
- private static ISearchManager mService;
-
- static {
- mService = ISearchManager.Stub.asInterface(
- ServiceManager.getService(Context.SEARCH_SERVICE));
- }
/**
* Launch search UI.
@@ -1459,5 +1557,93 @@ public class SearchManager
mSearchDialog.onConfigurationChanged(newConfig);
}
}
-
+
+ private static ISearchManager getSearchManagerService() {
+ return ISearchManager.Stub.asInterface(
+ ServiceManager.getService(Context.SEARCH_SERVICE));
+ }
+
+ /**
+ * Gets information about a searchable activity. This method is static so that it can
+ * be used from non-Activity contexts.
+ *
+ * @param componentName The activity to get searchable information for.
+ * @param globalSearch If <code>false</code>, return information about the given activity.
+ * If <code>true</code>, return information about the global search activity.
+ * @return Searchable information, or <code>null</code> if the activity is not searchable.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public static SearchableInfo getSearchableInfo(ComponentName componentName,
+ boolean globalSearch) {
+ try {
+ return sService.getSearchableInfo(componentName, globalSearch);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Checks whether the given searchable is the default searchable.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public static boolean isDefaultSearchable(SearchableInfo searchable) {
+ SearchableInfo defaultSearchable = SearchManager.getSearchableInfo(null, true);
+ return defaultSearchable != null
+ && defaultSearchable.mSearchActivity.equals(searchable.mSearchActivity);
+ }
+
+ /**
+ * Gets a cursor with search suggestions. This method is static so that it can
+ * be used from non-Activity context.
+ *
+ * @param searchable Information about how to get the suggestions.
+ * @param query The search text entered (so far).
+ * @return a cursor with suggestions, or <code>null</null> the suggestion query failed.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public static Cursor getSuggestions(Context context, SearchableInfo searchable, String query) {
+ if (searchable == null) {
+ return null;
+ }
+
+ String authority = searchable.getSuggestAuthority();
+ if (authority == null) {
+ return null;
+ }
+
+ Uri.Builder uriBuilder = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority);
+
+ // if content path provided, insert it now
+ final String contentPath = searchable.getSuggestPath();
+ if (contentPath != null) {
+ uriBuilder.appendEncodedPath(contentPath);
+ }
+
+ // append standard suggestion query path
+ uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
+
+ // get the query selection, may be null
+ String selection = searchable.getSuggestSelection();
+ // inject query, either as selection args or inline
+ String[] selArgs = null;
+ if (selection != null) { // use selection if provided
+ selArgs = new String[] { query };
+ } else { // no selection, use REST pattern
+ uriBuilder.appendPath(query);
+ }
+
+ Uri uri = uriBuilder
+ .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
+ .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
+ .build();
+
+ // finally, make the query
+ return context.getContentResolver().query(uri, null, selection, selArgs, null);
+ }
+
}
diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java
new file mode 100644
index 0000000..0513fe1
--- /dev/null
+++ b/core/java/android/app/SuggestionsAdapter.java
@@ -0,0 +1,344 @@
+/*
+ * 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.app;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.server.search.SearchableInfo;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+import java.io.FileNotFoundException;
+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 SearchableInfo mSearchable;
+ private Context mProviderContext;
+ private WeakHashMap<String, Drawable> mOutsideDrawablesCache;
+
+ // Cached column indexes, updated when the cursor changes.
+ private int mFormatCol;
+ private int mText1Col;
+ private int mText2Col;
+ private int mIconName1Col;
+ private int mIconName2Col;
+ private int mIconBitmap1Col;
+ private int mIconBitmap2Col;
+
+ public SuggestionsAdapter(Context context, SearchableInfo searchable,
+ WeakHashMap<String, Drawable> outsideDrawablesCache) {
+ super(context,
+ com.android.internal.R.layout.search_dropdown_item_icons_2line,
+ null, // no initial cursor
+ true); // auto-requery
+ mSearchable = searchable;
+
+ // set up provider resources (gives us icons, etc.)
+ Context activityContext = mSearchable.getActivityContext(mContext);
+ mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
+
+ mOutsideDrawablesCache = outsideDrawablesCache;
+ }
+
+ /**
+ * 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();
+ try {
+ return SearchManager.getSuggestions(mContext, mSearchable, query);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Cache columns.
+ */
+ @Override
+ public void changeCursor(Cursor c) {
+ if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
+ super.changeCursor(c);
+ if (c != null) {
+ mFormatCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT);
+ mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
+ mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
+ mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
+ mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
+ mIconBitmap1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1_BITMAP);
+ mIconBitmap2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2_BITMAP);
+ }
+ }
+
+ /**
+ * 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();
+ String format = cursor.getString(mFormatCol);
+ boolean isHtml = "html".equals(format);
+ setViewText(cursor, views.mText1, mText1Col, isHtml);
+ setViewText(cursor, views.mText2, mText2Col, isHtml);
+ setViewIcon(cursor, views.mIcon1, mIconBitmap1Col, mIconName1Col);
+ setViewIcon(cursor, views.mIcon2, mIconBitmap2Col, mIconName2Col);
+ }
+
+ private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) {
+ if (v == null) {
+ return;
+ }
+ CharSequence text = null;
+ if (textCol >= 0) {
+ String str = cursor.getString(textCol);
+ text = (str != null && isHtml) ? Html.fromHtml(str) : str;
+ }
+ // 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 void setViewIcon(Cursor cursor, ImageView v, int iconBitmapCol, int iconNameCol) {
+ if (v == null) {
+ return;
+ }
+ Drawable drawable = null;
+ // First try the bitmap column
+ if (iconBitmapCol >= 0) {
+ byte[] data = cursor.getBlob(iconBitmapCol);
+ if (data != null) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (bitmap != null) {
+ drawable = new BitmapDrawable(bitmap);
+ }
+ }
+ }
+ // If there was no bitmap, try the icon resource column.
+ if (drawable == null && iconNameCol >= 0) {
+ String value = cursor.getString(iconNameCol);
+ drawable = getDrawableFromResourceValue(value);
+ }
+ // 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);
+ }
+ }
+
+ /**
+ * 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.mQueryRewriteFromData) {
+ String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+ if (data != null) {
+ return data;
+ }
+ }
+
+ if (mSearchable.mQueryRewriteFromText) {
+ 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;
+ }
+
+ // First, check the cache.
+ Drawable drawable = mOutsideDrawablesCache.get(drawableId);
+ if (drawable != null) return drawable;
+
+ try {
+ // Not cached, try using it as a plain resource ID in the provider's context.
+ int resourceId = Integer.parseInt(drawableId);
+ drawable = mProviderContext.getResources().getDrawable(resourceId);
+ } catch (NumberFormatException nfe) {
+ // The id was not an integer resource id.
+ // Let the ContentResolver handle content, android.resource and file URIs.
+ try {
+ Uri uri = Uri.parse(drawableId);
+ drawable = Drawable.createFromStream(
+ mProviderContext.getContentResolver().openInputStream(uri),
+ null);
+ } catch (FileNotFoundException fnfe) {
+ // drawable = null;
+ }
+
+ // If we got a drawable for this resource id, then stick it in the
+ // map so we don't do this lookup again.
+ if (drawable != null) {
+ mOutsideDrawablesCache.put(drawableId, drawable);
+ }
+ } catch (NotFoundException nfe) {
+ // Resource could not be found
+ // drawable = 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);
+ if (col == -1) {
+ return null;
+ }
+ return cursor.getString(col);
+ }
+
+}
diff --git a/core/java/android/provider/Applications.java b/core/java/android/provider/Applications.java
new file mode 100644
index 0000000..0b0ce58
--- /dev/null
+++ b/core/java/android/provider/Applications.java
@@ -0,0 +1,82 @@
+/*
+ * 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.provider;
+
+import android.app.SearchManager;
+import android.net.Uri;
+import android.widget.SimpleCursorAdapter;
+
+/**
+ * <p>The Applications provider gives information about installed applications.</p>
+ *
+ * <p>This provider provides the following columns:
+ *
+ * <table border="2" width="85%" align="center" frame="hsides" rules="rows">
+ *
+ * <thead>
+ * <tr><th>Column Name</th> <th>Description</th> </tr>
+ * </thead>
+ *
+ * <tbody>
+ * <tr><th>{@link SearchManager#SUGGEST_COLUMN_TEXT_1}</th>
+ * <td>The application name.</td>
+ * </tr>
+ *
+ * <tr><th>{@link SearchManager#SUGGEST_COLUMN_INTENT_COMPONENT}</th>
+ * <td>The component to be used when forming the intent.</td>
+ * </tr>
+ *
+ * <tr><th>{@link SearchManager#SUGGEST_COLUMN_ICON_1}</th>
+ * <td>The application's icon resource id, prepended by its package name and
+ * separated by a colon, e.g., "com.android.alarmclock:2130837524". The
+ * package name is required for an activity interpreting this value to
+ * be able to correctly access the icon drawable, for example, in an override of
+ * {@link SimpleCursorAdapter#setViewImage(android.widget.ImageView, String)}.</td>
+ * </tr>
+ *
+ * <tr><th>{@link SearchManager#SUGGEST_COLUMN_ICON_2}</th>
+ * <td><i>Unused - column provided to conform to the {@link SearchManager} stipulation
+ * that all providers provide either both or neither of
+ * {@link SearchManager#SUGGEST_COLUMN_ICON_1} and
+ * {@link SearchManager#SUGGEST_COLUMN_ICON_2}.</td>
+ * </tr>
+ *
+ * @hide pending API council approval - should be unhidden at the same time as
+ * {@link SearchManager#SUGGEST_COLUMN_INTENT_COMPONENT}
+ */
+public class Applications {
+ private static final String TAG = "Applications";
+
+ /**
+ * The content authority for this provider.
+ *
+ * @hide
+ */
+ public static final String AUTHORITY = "applications";
+
+ /**
+ * The content:// style URL for this provider
+ *
+ * @hide
+ */
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
+
+ /**
+ * no public constructor since this is a utility class
+ */
+ private Applications() {}
+}
diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java
index fe15553..eaace6b 100644
--- a/core/java/android/server/search/SearchManagerService.java
+++ b/core/java/android/server/search/SearchManagerService.java
@@ -22,8 +22,8 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Handler;
-import android.util.Config;
/**
* This is a simplified version of the Search Manager service. It no longer handles
@@ -36,7 +36,6 @@ public class SearchManagerService extends ISearchManager.Stub
// general debugging support
private static final String TAG = "SearchManagerService";
private static final boolean DEBUG = false;
- private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV;
// configuration choices
private static final boolean IMMEDIATE_SEARCHABLES_UPDATE = true;
@@ -45,9 +44,10 @@ public class SearchManagerService extends ISearchManager.Stub
private final Context mContext;
private final Handler mHandler;
private boolean mSearchablesDirty;
+ private Searchables mSearchables;
/**
- * Initialize the Search Manager service in the provided system context.
+ * Initializes the Search Manager service in the provided system context.
* Only one instance of this object should be created!
*
* @param context to use for accessing DB, window manager, etc.
@@ -55,6 +55,8 @@ public class SearchManagerService extends ISearchManager.Stub
public SearchManagerService(Context context) {
mContext = context;
mHandler = new Handler();
+ mSearchablesDirty = true;
+ mSearchables = new Searchables(context);
// Setup the infrastructure for updating and maintaining the list
// of searchable activities.
@@ -64,7 +66,6 @@ public class SearchManagerService extends ISearchManager.Stub
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
- mSearchablesDirty = true;
// After startup settles down, preload the searchables list,
// which will reduce the delay when the search UI is invoked.
@@ -109,34 +110,41 @@ public class SearchManagerService extends ISearchManager.Stub
};
/**
- * Update the list of searchables, either at startup or in response to
+ * Updates the list of searchables, either at startup or in response to
* a package add/remove broadcast message.
*/
private void updateSearchables() {
- SearchableInfo.buildSearchableList(mContext);
+ mSearchables.buildSearchableList();
mSearchablesDirty = false;
- // TODO This is a hack. This shouldn't be hardcoded here, it's probably
- // a policy.
-// ComponentName defaultSearch = new ComponentName(
-// "com.android.contacts",
-// "com.android.contacts.ContactsListActivity" );
- ComponentName defaultSearch = new ComponentName(
- "com.android.googlesearch",
- "com.android.googlesearch.GoogleSearch" );
- SearchableInfo.setDefaultSearchable(mContext, defaultSearch);
+ // TODO SearchableInfo should be the source of truth about whether a searchable exists.
+ // As it stands, if the package exists but is misconfigured in some way, then this
+ // would fail, and needs to be fixed.
+ ComponentName defaultSearch = new ComponentName(
+ "com.android.globalsearch",
+ "com.android.globalsearch.GlobalSearch");
+
+ try {
+ mContext.getPackageManager().getActivityInfo(defaultSearch, 0);
+ } catch (NameNotFoundException e) {
+ defaultSearch = new ComponentName(
+ "com.android.googlesearch",
+ "com.android.googlesearch.GoogleSearch");
+ }
+
+ mSearchables.setDefaultSearchable(defaultSearch);
}
/**
- * Return the searchableinfo for a given activity
+ * Returns the SearchableInfo for a given activity
*
* @param launchActivity The activity from which we're launching this search.
- * @return Returns a SearchableInfo record describing the parameters of the search,
- * or null if no searchable metadata was available.
* @param globalSearch If false, this will only launch the search that has been specifically
* defined by the application (which is usually defined as a local search). If no default
* search is defined in the current application or activity, no search will be launched.
* If true, this will always launch a platform-global (e.g. web-based) search instead.
+ * @return Returns a SearchableInfo record describing the parameters of the search,
+ * or null if no searchable metadata was available.
*/
public SearchableInfo getSearchableInfo(ComponentName launchActivity, boolean globalSearch) {
// final check. however we should try to avoid this, because
@@ -146,11 +154,12 @@ public class SearchManagerService extends ISearchManager.Stub
}
SearchableInfo si = null;
if (globalSearch) {
- si = SearchableInfo.getDefaultSearchable();
+ si = mSearchables.getDefaultSearchable();
} else {
- si = SearchableInfo.getSearchableInfo(mContext, launchActivity);
+ si = mSearchables.getSearchableInfo(launchActivity);
}
return si;
}
+
}
diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java
index 0c04839..22abd1b 100644
--- a/core/java/android/server/search/SearchableInfo.java
+++ b/core/java/android/server/search/SearchableInfo.java
@@ -21,14 +21,11 @@ import org.xmlpull.v1.XmlPullParserException;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
-import android.content.pm.ResolveInfo;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
-import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.InputType;
@@ -38,9 +35,6 @@ import android.util.Xml;
import android.view.inputmethod.EditorInfo;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
public final class SearchableInfo implements Parcelable {
@@ -50,19 +44,12 @@ public final class SearchableInfo implements Parcelable {
// set this flag to 1 to prevent any apps from providing suggestions
final static int DBG_INHIBIT_SUGGESTIONS = 0;
- // static strings used for XML lookups, etc.
+ // static strings used for XML lookups.
// TODO how should these be documented for the developer, in a more structured way than
// the current long wordy javadoc in SearchManager.java ?
- private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
private static final String MD_LABEL_SEARCHABLE = "android.app.searchable";
- private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
private static final String MD_XML_ELEMENT_SEARCHABLE = "searchable";
private static final String MD_XML_ELEMENT_SEARCHABLE_ACTION_KEY = "actionkey";
-
- // class maintenance and general shared data
- private static HashMap<ComponentName, SearchableInfo> sSearchablesMap = null;
- private static ArrayList<SearchableInfo> sSearchablesList = null;
- private static SearchableInfo sDefaultSearchable = null;
// true member variables - what we know about the searchability
// TO-DO replace public with getters
@@ -86,7 +73,6 @@ public final class SearchableInfo implements Parcelable {
private String mSuggestIntentData = null;
private ActionKeyInfo mActionKeyList = null;
private String mSuggestProviderPackage = null;
- private Context mCacheActivityContext = null; // use during setup only - don't hold memory!
// Flag values for Searchable_voiceSearchMode
private static int VOICE_SEARCH_SHOW_BUTTON = 1;
@@ -97,37 +83,7 @@ public final class SearchableInfo implements Parcelable {
private int mVoicePromptTextId; // voicePromptText
private int mVoiceLanguageId; // voiceLanguage
private int mVoiceMaxResults; // voiceMaxResults
-
- /**
- * Set the default searchable activity (when none is specified).
- */
- public static void setDefaultSearchable(Context context,
- ComponentName activity) {
- synchronized (SearchableInfo.class) {
- SearchableInfo si = null;
- if (activity != null) {
- si = getSearchableInfo(context, activity);
- if (si != null) {
- // move to front of list
- sSearchablesList.remove(si);
- sSearchablesList.add(0, si);
- }
- }
- sDefaultSearchable = si;
- }
- }
-
- /**
- * Provides the system-default search activity, which you can use
- * whenever getSearchableInfo() returns null;
- *
- * @return Returns the system-default search activity, null if never defined
- */
- public static SearchableInfo getDefaultSearchable() {
- synchronized (SearchableInfo.class) {
- return sDefaultSearchable;
- }
- }
+
/**
* Retrieve the authority for obtaining search suggestions.
@@ -193,9 +149,16 @@ public final class SearchableInfo implements Parcelable {
* @return Returns a context related to the searchable activity
*/
public Context getActivityContext(Context context) {
+ return createActivityContext(context, mSearchActivity);
+ }
+
+ /**
+ * Creates a context for another activity.
+ */
+ private static Context createActivityContext(Context context, ComponentName activity) {
Context theirContext = null;
try {
- theirContext = context.createPackageContext(mSearchActivity.getPackageName(), 0);
+ theirContext = context.createPackageContext(activity.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
// unexpected, but we deal with this by null-checking theirContext
} catch (java.lang.SecurityException e) {
@@ -234,242 +197,68 @@ public final class SearchableInfo implements Parcelable {
}
/**
- * Factory. Look up, or construct, based on the activity.
- *
- * The activities fall into three cases, based on meta-data found in
- * the manifest entry:
- * <ol>
- * <li>The activity itself implements search. This is indicated by the
- * presence of a "android.app.searchable" meta-data attribute.
- * The value is a reference to an XML file containing search information.</li>
- * <li>A related activity implements search. This is indicated by the
- * presence of a "android.app.default_searchable" meta-data attribute.
- * The value is a string naming the activity implementing search. In this
- * case the factory will "redirect" and return the searchable data.</li>
- * <li>No searchability data is provided. We return null here and other
- * code will insert the "default" (e.g. contacts) search.
- *
- * TODO: cache the result in the map, and check the map first.
- * TODO: it might make sense to implement the searchable reference as
- * an application meta-data entry. This way we don't have to pepper each
- * and every activity.
- * TODO: can we skip the constructor step if it's a non-searchable?
- * TODO: does it make sense to plug the default into a slot here for
- * automatic return? Probably not, but it's one way to do it.
- *
- * @param activity The name of the current activity, or null if the
- * activity does not define any explicit searchable metadata.
- */
- public static SearchableInfo getSearchableInfo(Context context,
- ComponentName activity) {
- // Step 1. Is the result already hashed? (case 1)
- SearchableInfo result;
- synchronized (SearchableInfo.class) {
- result = sSearchablesMap.get(activity);
- if (result != null) return result;
- }
-
- // Step 2. See if the current activity references a searchable.
- // Note: Conceptually, this could be a while(true) loop, but there's
- // no point in implementing reference chaining here and risking a loop.
- // References must point directly to searchable activities.
-
- ActivityInfo ai = null;
- XmlPullParser xml = null;
- try {
- ai = context.getPackageManager().
- getActivityInfo(activity, PackageManager.GET_META_DATA );
- String refActivityName = null;
-
- // First look for activity-specific reference
- Bundle md = ai.metaData;
- if (md != null) {
- refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
- }
- // If not found, try for app-wide reference
- if (refActivityName == null) {
- md = ai.applicationInfo.metaData;
- if (md != null) {
- refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
- }
- }
-
- // Irrespective of source, if a reference was found, follow it.
- if (refActivityName != null)
- {
- // An app or activity can declare that we should simply launch
- // "system default search" if search is invoked.
- if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
- return getDefaultSearchable();
- }
- String pkg = activity.getPackageName();
- ComponentName referredActivity;
- if (refActivityName.charAt(0) == '.') {
- referredActivity = new ComponentName(pkg, pkg + refActivityName);
- } else {
- referredActivity = new ComponentName(pkg, refActivityName);
- }
-
- // Now try the referred activity, and if found, cache
- // it against the original name so we can skip the check
- synchronized (SearchableInfo.class) {
- result = sSearchablesMap.get(referredActivity);
- if (result != null) {
- sSearchablesMap.put(activity, result);
- return result;
- }
- }
- }
- } catch (PackageManager.NameNotFoundException e) {
- // case 3: no metadata
- }
-
- // Step 3. None found. Return null.
- return null;
-
- }
-
- /**
- * Super-factory. Builds an entire list (suitable for display) of
- * activities that are searchable, by iterating the entire set of
- * ACTION_SEARCH intents.
- *
- * Also clears the hash of all activities -> searches which will
- * refill as the user clicks "search".
- *
- * This should only be done at startup and again if we know that the
- * list has changed.
- *
- * TODO: every activity that provides a ACTION_SEARCH intent should
- * also provide searchability meta-data. There are a bunch of checks here
- * that, if data is not found, silently skip to the next activity. This
- * won't help a developer trying to figure out why their activity isn't
- * showing up in the list, but an exception here is too rough. I would
- * like to find a better notification mechanism.
- *
- * TODO: sort the list somehow? UI choice.
- *
- * @param context a context we can use during this work
- */
- public static void buildSearchableList(Context context) {
-
- // create empty hash & list
- HashMap<ComponentName, SearchableInfo> newSearchablesMap
- = new HashMap<ComponentName, SearchableInfo>();
- ArrayList<SearchableInfo> newSearchablesList
- = new ArrayList<SearchableInfo>();
-
- // use intent resolver to generate list of ACTION_SEARCH receivers
- final PackageManager pm = context.getPackageManager();
- List<ResolveInfo> infoList;
- final Intent intent = new Intent(Intent.ACTION_SEARCH);
- infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
-
- // analyze each one, generate a Searchables record, and record
- if (infoList != null) {
- int count = infoList.size();
- for (int ii = 0; ii < count; ii++) {
- // for each component, try to find metadata
- ResolveInfo info = infoList.get(ii);
- ActivityInfo ai = info.activityInfo;
- XmlResourceParser xml = ai.loadXmlMetaData(context.getPackageManager(),
- MD_LABEL_SEARCHABLE);
- if (xml == null) {
- continue;
- }
- ComponentName cName = new ComponentName(
- info.activityInfo.packageName,
- info.activityInfo.name);
-
- SearchableInfo searchable = getActivityMetaData(context, xml, cName);
- xml.close();
-
- if (searchable != null) {
- // no need to keep the context any longer. setup time is over.
- searchable.mCacheActivityContext = null;
-
- newSearchablesList.add(searchable);
- newSearchablesMap.put(cName, searchable);
- }
- }
- }
-
- // record the final values as a coherent pair
- synchronized (SearchableInfo.class) {
- sSearchablesList = newSearchablesList;
- sSearchablesMap = newSearchablesMap;
- }
- }
-
- /**
* Constructor
*
* Given a ComponentName, get the searchability info
* and build a local copy of it. Use the factory, not this.
*
- * @param context runtime context
+ * @param activityContext runtime context for the activity that the searchable info is about.
* @param attr The attribute set we found in the XML file, contains the values that are used to
* construct the object.
* @param cName The component name of the searchable activity
*/
- private SearchableInfo(Context context, AttributeSet attr, final ComponentName cName) {
+ private SearchableInfo(Context activityContext, AttributeSet attr, final ComponentName cName) {
// initialize as an "unsearchable" object
mSearchable = false;
mSearchActivity = cName;
- // to access another activity's resources, I need its context.
- // BE SURE to release the cache sometime after construction - it's a large object to hold
- mCacheActivityContext = getActivityContext(context);
- if (mCacheActivityContext != null) {
- TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr,
- com.android.internal.R.styleable.Searchable);
- mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0);
- mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0);
- mHintId = a.getResourceId(com.android.internal.R.styleable.Searchable_hint, 0);
- mIconId = a.getResourceId(com.android.internal.R.styleable.Searchable_icon, 0);
- mSearchButtonText = a.getResourceId(
- com.android.internal.R.styleable.Searchable_searchButtonText, 0);
- mSearchInputType = a.getInt(com.android.internal.R.styleable.Searchable_inputType,
- InputType.TYPE_CLASS_TEXT |
- InputType.TYPE_TEXT_VARIATION_NORMAL);
- mSearchImeOptions = a.getInt(com.android.internal.R.styleable.Searchable_imeOptions,
- EditorInfo.IME_ACTION_SEARCH);
+ TypedArray a = activityContext.obtainStyledAttributes(attr,
+ com.android.internal.R.styleable.Searchable);
+ mSearchMode = a.getInt(com.android.internal.R.styleable.Searchable_searchMode, 0);
+ mLabelId = a.getResourceId(com.android.internal.R.styleable.Searchable_label, 0);
+ mHintId = a.getResourceId(com.android.internal.R.styleable.Searchable_hint, 0);
+ mIconId = a.getResourceId(com.android.internal.R.styleable.Searchable_icon, 0);
+ mSearchButtonText = a.getResourceId(
+ com.android.internal.R.styleable.Searchable_searchButtonText, 0);
+ mSearchInputType = a.getInt(com.android.internal.R.styleable.Searchable_inputType,
+ InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_VARIATION_NORMAL);
+ mSearchImeOptions = a.getInt(com.android.internal.R.styleable.Searchable_imeOptions,
+ EditorInfo.IME_ACTION_SEARCH);
- setSearchModeFlags();
- if (DBG_INHIBIT_SUGGESTIONS == 0) {
- mSuggestAuthority = a.getString(
- com.android.internal.R.styleable.Searchable_searchSuggestAuthority);
- mSuggestPath = a.getString(
- com.android.internal.R.styleable.Searchable_searchSuggestPath);
- mSuggestSelection = a.getString(
- com.android.internal.R.styleable.Searchable_searchSuggestSelection);
- mSuggestIntentAction = a.getString(
- com.android.internal.R.styleable.Searchable_searchSuggestIntentAction);
- mSuggestIntentData = a.getString(
- com.android.internal.R.styleable.Searchable_searchSuggestIntentData);
- }
- mVoiceSearchMode =
- a.getInt(com.android.internal.R.styleable.Searchable_voiceSearchMode, 0);
- // TODO this didn't work - came back zero from YouTube
- mVoiceLanguageModeId =
- a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguageModel, 0);
- mVoicePromptTextId =
- a.getResourceId(com.android.internal.R.styleable.Searchable_voicePromptText, 0);
- mVoiceLanguageId =
- a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguage, 0);
- mVoiceMaxResults =
- a.getInt(com.android.internal.R.styleable.Searchable_voiceMaxResults, 0);
+ setSearchModeFlags();
+ if (DBG_INHIBIT_SUGGESTIONS == 0) {
+ mSuggestAuthority = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestAuthority);
+ mSuggestPath = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestPath);
+ mSuggestSelection = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestSelection);
+ mSuggestIntentAction = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestIntentAction);
+ mSuggestIntentData = a.getString(
+ com.android.internal.R.styleable.Searchable_searchSuggestIntentData);
+ }
+ mVoiceSearchMode =
+ a.getInt(com.android.internal.R.styleable.Searchable_voiceSearchMode, 0);
+ // TODO this didn't work - came back zero from YouTube
+ mVoiceLanguageModeId =
+ a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguageModel, 0);
+ mVoicePromptTextId =
+ a.getResourceId(com.android.internal.R.styleable.Searchable_voicePromptText, 0);
+ mVoiceLanguageId =
+ a.getResourceId(com.android.internal.R.styleable.Searchable_voiceLanguage, 0);
+ mVoiceMaxResults =
+ a.getInt(com.android.internal.R.styleable.Searchable_voiceMaxResults, 0);
- a.recycle();
+ a.recycle();
- // get package info for suggestions provider (if any)
- if (mSuggestAuthority != null) {
- ProviderInfo pi =
- context.getPackageManager().resolveContentProvider(mSuggestAuthority,
- 0);
- if (pi != null) {
- mSuggestProviderPackage = pi.packageName;
- }
+ // get package info for suggestions provider (if any)
+ if (mSuggestAuthority != null) {
+ PackageManager pm = activityContext.getPackageManager();
+ ProviderInfo pi = pm.resolveContentProvider(mSuggestAuthority, 0);
+ if (pi != null) {
+ mSuggestProviderPackage = pi.packageName;
}
}
@@ -496,7 +285,7 @@ public final class SearchableInfo implements Parcelable {
/**
* Private class used to hold the "action key" configuration
*/
- public class ActionKeyInfo implements Parcelable {
+ public static class ActionKeyInfo implements Parcelable {
public int mKeyCode = 0;
public String mQueryActionMsg;
@@ -506,14 +295,15 @@ public final class SearchableInfo implements Parcelable {
/**
* Create one object using attributeset as input data.
- * @param context runtime context
+ * @param activityContext runtime context of the activity that the action key information
+ * is about.
* @param attr The attribute set we found in the XML file, contains the values that are used to
* construct the object.
* @param next We'll build these up using a simple linked list (since there are usually
* just zero or one).
*/
- public ActionKeyInfo(Context context, AttributeSet attr, ActionKeyInfo next) {
- TypedArray a = mCacheActivityContext.obtainStyledAttributes(attr,
+ public ActionKeyInfo(Context activityContext, AttributeSet attr, ActionKeyInfo next) {
+ TypedArray a = activityContext.obtainStyledAttributes(attr,
com.android.internal.R.styleable.SearchableActionKey);
mKeyCode = a.getInt(
@@ -584,6 +374,20 @@ public final class SearchableInfo implements Parcelable {
return null;
}
+ public static SearchableInfo getActivityMetaData(Context context, ActivityInfo activityInfo) {
+ // for each component, try to find metadata
+ XmlResourceParser xml =
+ activityInfo.loadXmlMetaData(context.getPackageManager(), MD_LABEL_SEARCHABLE);
+ if (xml == null) {
+ return null;
+ }
+ ComponentName cName = new ComponentName(activityInfo.packageName, activityInfo.name);
+
+ SearchableInfo searchable = getActivityMetaData(context, xml, cName);
+ xml.close();
+ return searchable;
+ }
+
/**
* Get the metadata for a given activity
*
@@ -598,6 +402,7 @@ public final class SearchableInfo implements Parcelable {
private static SearchableInfo getActivityMetaData(Context context, XmlPullParser xml,
final ComponentName cName) {
SearchableInfo result = null;
+ Context activityContext = createActivityContext(context, cName);
// in order to use the attributes mechanism, we have to walk the parser
// forward through the file until it's reading the tag of interest.
@@ -608,7 +413,7 @@ public final class SearchableInfo implements Parcelable {
if (xml.getName().equals(MD_XML_ELEMENT_SEARCHABLE)) {
AttributeSet attr = Xml.asAttributeSet(xml);
if (attr != null) {
- result = new SearchableInfo(context, attr, cName);
+ result = new SearchableInfo(activityContext, attr, cName);
// if the constructor returned a bad object, exit now.
if (! result.mSearchable) {
return null;
@@ -621,7 +426,7 @@ public final class SearchableInfo implements Parcelable {
}
AttributeSet attr = Xml.asAttributeSet(xml);
if (attr != null) {
- ActionKeyInfo keyInfo = result.new ActionKeyInfo(context, attr,
+ ActionKeyInfo keyInfo = new ActionKeyInfo(activityContext, attr,
result.mActionKeyList);
// only add to list if it is was useable
if (keyInfo.mKeyCode != 0) {
@@ -637,6 +442,7 @@ public final class SearchableInfo implements Parcelable {
} catch (IOException e) {
throw new RuntimeException(e);
}
+
return result;
}
@@ -757,16 +563,6 @@ public final class SearchableInfo implements Parcelable {
}
/**
- * Return the list of searchable activities, for use in the drop-down.
- */
- public static ArrayList<SearchableInfo> getSearchablesList() {
- synchronized (SearchableInfo.class) {
- ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(sSearchablesList);
- return result;
- }
- }
-
- /**
* Support for parcelable and aidl operations.
*/
public static final Parcelable.Creator<SearchableInfo> CREATOR
diff --git a/core/java/android/server/search/Searchables.java b/core/java/android/server/search/Searchables.java
new file mode 100644
index 0000000..ba75d21
--- /dev/null
+++ b/core/java/android/server/search/Searchables.java
@@ -0,0 +1,243 @@
+/*
+ * 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.server.search;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This class maintains the information about all searchable activities.
+ */
+public class Searchables {
+
+ // static strings used for XML lookups, etc.
+ // TODO how should these be documented for the developer, in a more structured way than
+ // the current long wordy javadoc in SearchManager.java ?
+ private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable";
+ private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*";
+
+ private Context mContext;
+
+ private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null;
+ private ArrayList<SearchableInfo> mSearchablesList = null;
+ private SearchableInfo mDefaultSearchable = null;
+
+ /**
+ *
+ * @param context Context to use for looking up activities etc.
+ */
+ public Searchables (Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Look up, or construct, based on the activity.
+ *
+ * The activities fall into three cases, based on meta-data found in
+ * the manifest entry:
+ * <ol>
+ * <li>The activity itself implements search. This is indicated by the
+ * presence of a "android.app.searchable" meta-data attribute.
+ * The value is a reference to an XML file containing search information.</li>
+ * <li>A related activity implements search. This is indicated by the
+ * presence of a "android.app.default_searchable" meta-data attribute.
+ * The value is a string naming the activity implementing search. In this
+ * case the factory will "redirect" and return the searchable data.</li>
+ * <li>No searchability data is provided. We return null here and other
+ * code will insert the "default" (e.g. contacts) search.
+ *
+ * TODO: cache the result in the map, and check the map first.
+ * TODO: it might make sense to implement the searchable reference as
+ * an application meta-data entry. This way we don't have to pepper each
+ * and every activity.
+ * TODO: can we skip the constructor step if it's a non-searchable?
+ * TODO: does it make sense to plug the default into a slot here for
+ * automatic return? Probably not, but it's one way to do it.
+ *
+ * @param activity The name of the current activity, or null if the
+ * activity does not define any explicit searchable metadata.
+ */
+ public SearchableInfo getSearchableInfo(ComponentName activity) {
+ // Step 1. Is the result already hashed? (case 1)
+ SearchableInfo result;
+ synchronized (this) {
+ result = mSearchablesMap.get(activity);
+ if (result != null) return result;
+ }
+
+ // Step 2. See if the current activity references a searchable.
+ // Note: Conceptually, this could be a while(true) loop, but there's
+ // no point in implementing reference chaining here and risking a loop.
+ // References must point directly to searchable activities.
+
+ ActivityInfo ai = null;
+ try {
+ ai = mContext.getPackageManager().
+ getActivityInfo(activity, PackageManager.GET_META_DATA );
+ String refActivityName = null;
+
+ // First look for activity-specific reference
+ Bundle md = ai.metaData;
+ if (md != null) {
+ refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
+ }
+ // If not found, try for app-wide reference
+ if (refActivityName == null) {
+ md = ai.applicationInfo.metaData;
+ if (md != null) {
+ refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE);
+ }
+ }
+
+ // Irrespective of source, if a reference was found, follow it.
+ if (refActivityName != null)
+ {
+ // An app or activity can declare that we should simply launch
+ // "system default search" if search is invoked.
+ if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) {
+ return getDefaultSearchable();
+ }
+ String pkg = activity.getPackageName();
+ ComponentName referredActivity;
+ if (refActivityName.charAt(0) == '.') {
+ referredActivity = new ComponentName(pkg, pkg + refActivityName);
+ } else {
+ referredActivity = new ComponentName(pkg, refActivityName);
+ }
+
+ // Now try the referred activity, and if found, cache
+ // it against the original name so we can skip the check
+ synchronized (this) {
+ result = mSearchablesMap.get(referredActivity);
+ if (result != null) {
+ mSearchablesMap.put(activity, result);
+ return result;
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // case 3: no metadata
+ }
+
+ // Step 3. None found. Return null.
+ return null;
+
+ }
+
+ /**
+ * Set the default searchable activity (when none is specified).
+ */
+ public synchronized void setDefaultSearchable(ComponentName activity) {
+ SearchableInfo si = null;
+ if (activity != null) {
+ si = getSearchableInfo(activity);
+ if (si != null) {
+ // move to front of list
+ mSearchablesList.remove(si);
+ mSearchablesList.add(0, si);
+ }
+ }
+ mDefaultSearchable = si;
+ }
+
+ /**
+ * Provides the system-default search activity, which you can use
+ * whenever getSearchableInfo() returns null;
+ *
+ * @return Returns the system-default search activity, null if never defined
+ */
+ public synchronized SearchableInfo getDefaultSearchable() {
+ return mDefaultSearchable;
+ }
+
+ public synchronized boolean isDefaultSearchable(SearchableInfo searchable) {
+ return searchable == mDefaultSearchable;
+ }
+
+ /**
+ * Builds an entire list (suitable for display) of
+ * activities that are searchable, by iterating the entire set of
+ * ACTION_SEARCH intents.
+ *
+ * Also clears the hash of all activities -> searches which will
+ * refill as the user clicks "search".
+ *
+ * This should only be done at startup and again if we know that the
+ * list has changed.
+ *
+ * TODO: every activity that provides a ACTION_SEARCH intent should
+ * also provide searchability meta-data. There are a bunch of checks here
+ * that, if data is not found, silently skip to the next activity. This
+ * won't help a developer trying to figure out why their activity isn't
+ * showing up in the list, but an exception here is too rough. I would
+ * like to find a better notification mechanism.
+ *
+ * TODO: sort the list somehow? UI choice.
+ */
+ public void buildSearchableList() {
+
+ // create empty hash & list
+ HashMap<ComponentName, SearchableInfo> newSearchablesMap
+ = new HashMap<ComponentName, SearchableInfo>();
+ ArrayList<SearchableInfo> newSearchablesList
+ = new ArrayList<SearchableInfo>();
+
+ // use intent resolver to generate list of ACTION_SEARCH receivers
+ final PackageManager pm = mContext.getPackageManager();
+ List<ResolveInfo> infoList;
+ final Intent intent = new Intent(Intent.ACTION_SEARCH);
+ infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
+
+ // analyze each one, generate a Searchables record, and record
+ if (infoList != null) {
+ int count = infoList.size();
+ for (int ii = 0; ii < count; ii++) {
+ // for each component, try to find metadata
+ ResolveInfo info = infoList.get(ii);
+ ActivityInfo ai = info.activityInfo;
+ SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai);
+ if (searchable != null) {
+ newSearchablesList.add(searchable);
+ newSearchablesMap.put(searchable.mSearchActivity, searchable);
+ }
+ }
+ }
+
+ // record the final values as a coherent pair
+ synchronized (this) {
+ mSearchablesList = newSearchablesList;
+ mSearchablesMap = newSearchablesMap;
+ }
+ }
+
+ /**
+ * Returns the list of searchable activities.
+ */
+ public synchronized ArrayList<SearchableInfo> getSearchablesList() {
+ ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList);
+ return result;
+ }
+}
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 772ad89..b408f27 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -983,18 +983,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
mSelectorRect.setEmpty();
invalidate();
}
-
- /**
- * The list is empty and we need to change the layout, so *really* clear everything out.
- * @hide - for AutoCompleteTextView & SearchDialog only
- */
- /* package */ void resetListAndClearViews() {
- rememberSyncState();
- removeAllViewsInLayout();
- mRecycler.clear();
- mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
- requestLayout();
- }
@Override
protected int computeVerticalScrollExtent() {
diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java
index dfb971e..e3186b0 100644
--- a/core/java/android/widget/AutoCompleteTextView.java
+++ b/core/java/android/widget/AutoCompleteTextView.java
@@ -110,6 +110,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
private final DropDownItemClickListener mDropDownItemClickListener =
new DropDownItemClickListener();
+ private boolean mDropDownAlwaysVisible = false;
+
+ private boolean mDropDownDismissedOnCompletion = true;
+
private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
private boolean mOpenBefore;
@@ -211,6 +215,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
*
* @return the width for the drop down list
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
*/
public int getDropDownWidth() {
return mDropDownWidth;
@@ -222,6 +228,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
*
* @param width the width to use
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
*/
public void setDropDownWidth(int width) {
mDropDownWidth = width;
@@ -231,6 +239,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
*
* @return the view's id, or {@link View#NO_ID} if none specified
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
*/
public int getDropDownAnchor() {
return mDropDownAnchorId;
@@ -242,13 +252,173 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* loading a view which is not yet instantiated.</p>
*
* @param id the id to anchor the drop down list view to
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
*/
public void setDropDownAnchor(int id) {
mDropDownAnchorId = id;
mDropDownAnchorView = null;
}
+
+ /**
+ * <p>Gets the background of the auto-complete drop-down list.</p>
+ *
+ * @return the background drawable
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ *
+ * @hide Pending API council approval
+ */
+ public Drawable getDropDownBackground() {
+ return mPopup.getBackground();
+ }
+
+ /**
+ * <p>Sets the background of the auto-complete drop-down list.</p>
+ *
+ * @param d the drawable to set as the background
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownBackgroundDrawable(Drawable d) {
+ mPopup.setBackgroundDrawable(d);
+ }
+
+ /**
+ * <p>Sets the background of the auto-complete drop-down list.</p>
+ *
+ * @param id the id of the drawable to set as the background
+ *
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownBackgroundResource(int id) {
+ mPopup.setBackgroundDrawable(getResources().getDrawable(id));
+ }
+
+ /**
+ * <p>Sets the animation style of the auto-complete drop-down list.</p>
+ *
+ * <p>If the drop-down is showing, calling this method will take effect only
+ * the next time the drop-down is shown.</p>
+ *
+ * @param animationStyle animation style to use when the drop-down appears
+ * and disappears. Set to -1 for the default animation, 0 for no
+ * animation, or a resource identifier for an explicit animation.
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownAnimationStyle(int animationStyle) {
+ mPopup.setAnimationStyle(animationStyle);
+ }
/**
+ * <p>Returns the animation style that is used when the drop-down list appears and disappears
+ * </p>
+ *
+ * @return the animation style that is used when the drop-down list appears and disappears
+ *
+ * @hide Pending API council approval
+ */
+ public int getDropDownAnimationStyle() {
+ return mPopup.getAnimationStyle();
+ }
+
+ /**
+ * <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
+ *
+ * @param offset the vertical offset
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownVerticalOffset(int offset) {
+ mDropDownVerticalOffset = offset;
+ }
+
+ /**
+ * <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
+ *
+ * @return the vertical offset
+ *
+ * @hide Pending API council approval
+ */
+ public int getDropDownVerticalOffset() {
+ return mDropDownVerticalOffset;
+ }
+
+ /**
+ * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
+ *
+ * @param offset the horizontal offset
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownHorizontalOffset(int offset) {
+ mDropDownHorizontalOffset = offset;
+ }
+
+ /**
+ * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
+ *
+ * @return the horizontal offset
+ *
+ * @hide Pending API council approval
+ */
+ public int getDropDownHorizontalOffset() {
+ return mDropDownHorizontalOffset;
+ }
+
+ /**
+ * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
+ *
+ * @hide Pending API council approval
+ */
+ public boolean isDropDownAlwaysVisible() {
+ return mDropDownAlwaysVisible;
+ }
+
+ /**
+ * Sets whether the drop-down should remain visible as long as there is there is
+ * {@link #enoughToFilter()}. This is useful if an unknown number of results are expected
+ * to show up in the adapter sometime in the future.
+ *
+ * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
+ * of the size or content of the list. {@link #getDropDownBackground()} will fill any space
+ * that is not used by the list.
+ *
+ * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
+ mDropDownAlwaysVisible = dropDownAlwaysVisible;
+ }
+
+ /**
+ * Checks whether the drop-down is dismissed when a suggestion is clicked.
+ *
+ * @hide Pending API council approval
+ */
+ public boolean isDropDownDismissedOnCompletion() {
+ return mDropDownDismissedOnCompletion;
+ }
+
+ /**
+ * Sets whether the drop-down is dismissed when a suggestion is clicked. This is
+ * true by default.
+ *
+ * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
+ *
+ * @hide Pending API council approval
+ */
+ public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
+ mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
+ }
+
+ /**
* <p>Returns the number of characters the user must type before the drop
* down list is shown.</p>
*
@@ -628,16 +798,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
}
return ListView.INVALID_POSITION;
}
-
- /**
- * We're changing the adapter and its views so really, really clear everything out
- * @hide - for SearchDialog only
- */
- public void resetListAndClearViews() {
- if (mDropDownList != null) {
- mDropDownList.resetListAndClearViews();
- }
- }
/**
* <p>Starts filtering the content of the drop down list. The filtering
@@ -709,7 +869,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
}
}
- dismissDropDown();
+ if (mDropDownDismissedOnCompletion) {
+ dismissDropDown();
+ }
}
/**
@@ -721,6 +883,42 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
}
/**
+ * Like {@link #setText(CharSequence)}, except that it can disable filtering.
+ *
+ * @param filter If <code>false</code>, no filtering will be performed
+ * as a result of this call.
+ *
+ * @hide Pending API council approval.
+ */
+ public void setText(CharSequence text, boolean filter) {
+ if (filter) {
+ setText(text);
+ } else {
+ mBlockCompletion = true;
+ setText(text);
+ mBlockCompletion = false;
+ }
+ }
+
+ /**
+ * Like {@link #setTextKeepState(CharSequence)}, except that it can disable filtering.
+ *
+ * @param filter If <code>false</code>, no filtering will be performed
+ * as a result of this call.
+ *
+ * @hide Pending API council approval.
+ */
+ public void setTextKeepState(CharSequence text, boolean filter) {
+ if (filter) {
+ setTextKeepState(text);
+ } else {
+ mBlockCompletion = true;
+ setTextKeepState(text);
+ mBlockCompletion = false;
+ }
+ }
+
+ /**
* <p>Performs the text completion by replacing the current text by the
* selected item. Subclasses should override this method to avoid replacing
* the whole content of the edit box.</p>
@@ -734,6 +932,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
Selection.setSelection(spannable, spannable.length());
}
+ /** {@inheritDoc} */
public void onFilterComplete(int count) {
if (mAttachCount <= 0) return;
@@ -744,7 +943,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
* to filter.
*/
- if (count > 0 && enoughToFilter()) {
+ if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) {
if (hasFocus() && hasWindowFocus()) {
showDropDown();
}
@@ -809,22 +1008,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
}
/**
- * Set the horizontal offset with respect to {@link #setDropDownAnchor(int)}
- * @hide pending API council review
- */
- public void setDropDownHorizontalOffset(int horizontalOffset) {
- mDropDownHorizontalOffset = horizontalOffset;
- }
-
- /**
- * Set the vertical offset with respect to {@link #setDropDownAnchor(int)}
- * @hide pending API council review
- */
- public void setDropDownVerticalOffset(int verticalOffset) {
- mDropDownVerticalOffset = verticalOffset;
- }
-
- /**
* <p>Used for lazy instantiation of the anchor view from the id we have. If the value of
* the id is NO_ID or we can't find a view for the given id, we return this TextView as
* the default anchoring point.</p>
@@ -856,10 +1039,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
mDropDownVerticalOffset, widthSpec, height);
} else {
if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) {
- mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT, 0);
} else {
- mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopup.setWindowLayoutMode(0, 0);
if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
mPopup.setWidth(getDropDownAnchorView().getWidth());
} else {
@@ -966,8 +1148,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe
final int maxHeight = mPopup.getMaxAvailableHeight(this, mDropDownVerticalOffset);
//otherHeights += dropDownView.getPaddingTop() + dropDownView.getPaddingBottom();
- return mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
+ final int measuredHeight = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights;
+
+ return mDropDownAlwaysVisible ? maxHeight : measuredHeight;
}
private View getHintView(Context context) {
diff --git a/core/res/res/drawable/btn_global_search.xml b/core/res/res/drawable/btn_global_search.xml
new file mode 100644
index 0000000..531f07e
--- /dev/null
+++ b/core/res/res/drawable/btn_global_search.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- TODO Need different assets for some of these button states. -->
+ <item android:state_window_focused="false" android:state_enabled="true"
+ android:drawable="@drawable/btn_global_search_normal" />
+ <item android:state_window_focused="false" android:state_enabled="false"
+ android:drawable="@drawable/btn_global_search_normal" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/btn_default_pressed" />
+ <item android:state_focused="true" android:state_enabled="true"
+ android:drawable="@drawable/btn_default_selected" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/btn_global_search_normal" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/btn_global_search_normal" />
+ <item
+ android:drawable="@drawable/btn_global_search_normal" />
+</selector>
diff --git a/core/res/res/drawable/btn_global_search_normal.9.png b/core/res/res/drawable/btn_global_search_normal.9.png
new file mode 100644
index 0000000..9b7d3e5
--- /dev/null
+++ b/core/res/res/drawable/btn_global_search_normal.9.png
Binary files differ
diff --git a/core/res/res/drawable/btn_search_dialog.xml b/core/res/res/drawable/btn_search_dialog.xml
new file mode 100644
index 0000000..b7f5187
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_window_focused="false" android:state_enabled="true"
+ android:drawable="@drawable/btn_search_dialog_default" />
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/btn_search_dialog_pressed" />
+
+ <item android:state_focused="true" android:state_enabled="true"
+ android:drawable="@drawable/btn_search_dialog_selected" />
+
+ <item android:state_enabled="true"
+ android:drawable="@drawable/btn_search_dialog_default" />
+
+ <item
+ android:drawable="@drawable/btn_search_dialog_default" />
+</selector>
diff --git a/core/res/res/drawable/btn_search_dialog_default.9.png b/core/res/res/drawable/btn_search_dialog_default.9.png
new file mode 100644
index 0000000..ec39178
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog_default.9.png
Binary files differ
diff --git a/core/res/res/drawable/btn_search_dialog_pressed.9.png b/core/res/res/drawable/btn_search_dialog_pressed.9.png
new file mode 100644
index 0000000..5f52fef
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog_pressed.9.png
Binary files differ
diff --git a/core/res/res/drawable/btn_search_dialog_selected.9.png b/core/res/res/drawable/btn_search_dialog_selected.9.png
new file mode 100644
index 0000000..9fc2fde
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog_selected.9.png
Binary files differ
diff --git a/core/res/res/drawable/btn_search_dialog_voice.xml b/core/res/res/drawable/btn_search_dialog_voice.xml
new file mode 100644
index 0000000..748aaf5
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog_voice.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_window_focused="false" android:state_enabled="true"
+ android:drawable="@drawable/btn_search_dialog_voice_default" />
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/btn_search_dialog_voice_pressed" />
+
+ <item android:state_focused="true" android:state_enabled="true"
+ android:drawable="@drawable/btn_search_dialog_voice_selected" />
+
+ <item android:state_enabled="true"
+ android:drawable="@drawable/btn_search_dialog_voice_default" />
+
+ <item
+ android:drawable="@drawable/btn_search_dialog_voice_default" />
+</selector>
diff --git a/core/res/res/drawable/btn_search_dialog_voice_default.9.png b/core/res/res/drawable/btn_search_dialog_voice_default.9.png
new file mode 100644
index 0000000..2a3366c
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog_voice_default.9.png
Binary files differ
diff --git a/core/res/res/drawable/btn_search_dialog_voice_pressed.9.png b/core/res/res/drawable/btn_search_dialog_voice_pressed.9.png
new file mode 100644
index 0000000..57d7a74
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog_voice_pressed.9.png
Binary files differ
diff --git a/core/res/res/drawable/btn_search_dialog_voice_selected.9.png b/core/res/res/drawable/btn_search_dialog_voice_selected.9.png
new file mode 100644
index 0000000..db3187e
--- /dev/null
+++ b/core/res/res/drawable/btn_search_dialog_voice_selected.9.png
Binary files differ
diff --git a/core/res/res/drawable/search_dropdown_background.9.png b/core/res/res/drawable/search_dropdown_background.9.png
new file mode 100755
index 0000000..a6923b7
--- /dev/null
+++ b/core/res/res/drawable/search_dropdown_background.9.png
Binary files differ
diff --git a/core/res/res/drawable/search_plate_global.9.png b/core/res/res/drawable/search_plate_global.9.png
new file mode 100644
index 0000000..126054b
--- /dev/null
+++ b/core/res/res/drawable/search_plate_global.9.png
Binary files differ
diff --git a/core/res/res/drawable/textfield_search.xml b/core/res/res/drawable/textfield_search.xml
new file mode 100644
index 0000000..2923368
--- /dev/null
+++ b/core/res/res/drawable/textfield_search.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_window_focused="false" android:state_enabled="true"
+ android:drawable="@drawable/textfield_search_default" />
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/textfield_search_pressed" />
+
+ <item android:state_enabled="true" android:state_focused="true"
+ android:drawable="@drawable/textfield_search_selected" />
+
+ <item android:drawable="@drawable/textfield_search_default" />
+
+</selector>
+
diff --git a/core/res/res/drawable/textfield_search_default.9.png b/core/res/res/drawable/textfield_search_default.9.png
new file mode 100755
index 0000000..7dc5b27
--- /dev/null
+++ b/core/res/res/drawable/textfield_search_default.9.png
Binary files differ
diff --git a/core/res/res/drawable/textfield_search_pressed.9.png b/core/res/res/drawable/textfield_search_pressed.9.png
new file mode 100644
index 0000000..da00c25
--- /dev/null
+++ b/core/res/res/drawable/textfield_search_pressed.9.png
Binary files differ
diff --git a/core/res/res/drawable/textfield_search_selected.9.png b/core/res/res/drawable/textfield_search_selected.9.png
new file mode 100755
index 0000000..a9fd3b2
--- /dev/null
+++ b/core/res/res/drawable/textfield_search_selected.9.png
Binary files differ
diff --git a/core/res/res/layout/resolve_list_item.xml b/core/res/res/layout/resolve_list_item.xml
index 5e296c5..4c5c456 100644
--- a/core/res/res/layout/resolve_list_item.xml
+++ b/core/res/res/layout/resolve_list_item.xml
@@ -23,7 +23,7 @@
android:minHeight="?android:attr/listPreferredItemHeight"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
- android:paddingLeft="14dip"
+ android:paddingLeft="10dip"
android:paddingRight="15dip">
<!-- Activity icon when presenting dialog -->
@@ -42,13 +42,13 @@
android:textAppearance="?android:attr/textAppearanceLargeInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingLeft="6dip" />
+ android:paddingLeft="10dip" />
<!-- Extended activity info to distinguish between duplicate activity names -->
<TextView android:id="@android:id/text2"
android:textAppearance="?android:attr/textAppearanceMediumInverse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingLeft="6dip" />
+ android:paddingLeft="10dip" />
</LinearLayout>
</LinearLayout>
diff --git a/core/res/res/layout/search_bar.xml b/core/res/res/layout/search_bar.xml
index ef347da..6155626 100644
--- a/core/res/res/layout/search_bar.xml
+++ b/core/res/res/layout/search_bar.xml
@@ -26,79 +26,67 @@
android:orientation="vertical"
android:focusable="true"
android:descendantFocusability="afterDescendants">
- <!-- android:paddingBottom="14dip" TODO MUST FIX - it's a hack to get the popup to show -->
+ <!-- android:paddingBottom="200dip" TODO MUST FIX - it's a hack to get the popup to show -->
<!-- Outer layout defines the entire search bar at the top of the screen -->
- <!-- Bottom padding of 16 is due to the graphic, with 9 extra pixels of drop
- shadow, plus the desired padding of "8" against the user-visible (grey)
- pixels, minus "1" to correct for positioning of the edittext & button. -->
<LinearLayout
android:id="@+id/search_plate"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
- android:paddingLeft="8dip"
- android:paddingRight="8dip"
- android:paddingTop="6dip"
+ android:paddingLeft="12dip"
+ android:paddingRight="12dip"
+ android:paddingTop="7dip"
android:paddingBottom="16dip"
- android:baselineAligned="false"
- android:background="@android:drawable/search_plate"
- android:addStatesFromChildren="true" >
+ android:background="@drawable/search_plate_global" >
<!-- This is actually used for the badge icon *or* the badge label (or neither) -->
<TextView
android:id="@+id/search_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:paddingLeft="2dip"
+ android:layout_marginBottom="2dip"
android:drawablePadding="0dip"
android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:attr/textColorPrimary" />
+ android:textColor="?android:attr/textColorPrimaryInverse" />
<!-- Inner layout contains the button(s) and EditText -->
- <!-- The layout_marginTop of "1" corrects for the extra 1 pixel of padding at the top of
- textfield_selected.9.png. The "real" margin as displayed is "2". -->
- <!-- The layout_marginBottom of "-5" corrects for the spacing we see at the
- bottom of the edittext and button images. The "real" margin as displayed is "8" -->
<LinearLayout
android:id="@+id/search_edit_frame"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="1dip"
- android:layout_marginBottom="-5dip"
- android:orientation="horizontal"
- android:addStatesFromChildren="true"
- android:gravity="center_vertical"
- android:baselineAligned="false" >
-
+ android:orientation="horizontal">
+
<view class="android.app.SearchDialog$SearchAutoComplete"
android:id="@+id/search_src_text"
+ android:background="@drawable/textfield_search"
android:layout_height="wrap_content"
android:layout_width="0dip"
android:layout_weight="1.0"
android:paddingLeft="8dip"
android:paddingRight="6dip"
+ android:singleLine="true"
android:inputType="text|textAutoComplete"
android:dropDownWidth="fill_parent"
android:dropDownAnchor="@id/search_plate"
- android:dropDownVerticalOffset="-15dip"
+ android:dropDownVerticalOffset="-9dip"
+ android:popupBackground="@android:drawable/search_dropdown_background"
/>
- <!-- android:focusableInTouchMode="false" -->
- <!-- android:singleLine="true" -->
- <!-- android:selectAllOnFocus="true" -->
<!-- This button can switch between text and icon "modes" -->
<Button
android:id="@+id/search_go_btn"
- android:layout_marginLeft="1dip"
+ android:background="@drawable/btn_search_dialog"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:drawableLeft="@android:drawable/ic_btn_search"
+ android:layout_height="fill_parent"
/>
- <ImageButton android:id="@+id/search_voice_btn"
+ <ImageButton
+ android:id="@+id/search_voice_btn"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_marginLeft="8dip"
+ android:background="@drawable/btn_search_dialog_voice"
android:src="@android:drawable/ic_btn_speak_now"
/>
</LinearLayout>
diff --git a/core/res/res/layout/search_dropdown_item_1line.xml b/core/res/res/layout/search_dropdown_item_1line.xml
index 3827206..bf3dd48 100644
--- a/core/res/res/layout/search_dropdown_item_1line.xml
+++ b/core/res/res/layout/search_dropdown_item_1line.xml
@@ -20,7 +20,7 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
- android:textAppearance="?android:attr/textAppearanceMediumInverse"
+ android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
android:singleLine="true"
android:layout_width="fill_parent"
- android:layout_height="?android:attr/listPreferredItemHeight" />
+ android:layout_height="?android:attr/searchResultListItemHeight" /> \ No newline at end of file
diff --git a/core/res/res/layout/search_dropdown_item_2line.xml b/core/res/res/layout/search_dropdown_item_2line.xml
index 96d6005..5546b6636 100644
--- a/core/res/res/layout/search_dropdown_item_2line.xml
+++ b/core/res/res/layout/search_dropdown_item_2line.xml
@@ -20,15 +20,16 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
- android:layout_height="?android:attr/listPreferredItemHeight"
+ android:layout_height="?android:attr/searchResultListItemHeight"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
>
<TwoLineListItem
- android:paddingTop="2dip"
- android:paddingBottom="2dip"
+ android:paddingTop="1dip"
+ android:paddingBottom="1dip"
+ android:gravity="center_vertical"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
@@ -37,7 +38,7 @@
<TextView
android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
- android:textAppearance="?android:attr/textAppearanceMediumInverse"
+ android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
@@ -45,7 +46,7 @@
<TextView
android:id="@android:id/text2"
style="?android:attr/dropDownItemStyle"
- android:textAppearance="?android:attr/textAppearanceSmallInverse"
+ android:textAppearance="?android:attr/textAppearanceSearchResultSubtitle"
android:textColor="?android:attr/textColorSecondaryInverse"
android:singleLine="true"
android:layout_width="fill_parent"
diff --git a/core/res/res/layout/search_dropdown_item_icons_1line.xml b/core/res/res/layout/search_dropdown_item_icons_1line.xml
index c0713d5..4f65d74 100644
--- a/core/res/res/layout/search_dropdown_item_icons_1line.xml
+++ b/core/res/res/layout/search_dropdown_item_icons_1line.xml
@@ -22,31 +22,33 @@
<!-- of the text element in apps/common/res/layout/simple_dropdown_item_1line.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:paddingLeft="4dip"
+ android:paddingRight="2dip"
android:layout_width="fill_parent"
- android:layout_height="?android:attr/listPreferredItemHeight"
+ android:layout_height="?android:attr/searchResultListItemHeight"
android:orientation="horizontal"
android:gravity="center_vertical"
android:baselineAligned="false"
>
<ImageView android:id="@android:id/icon1"
- android:layout_width="32dip"
- android:layout_height="32dip"
+ android:layout_width="48dip"
+ android:layout_height="48dip"
android:layout_gravity="center_vertical"
- android:scaleType="fitCenter" />
+ android:scaleType="centerInside" />
<TextView android:id="@android:id/text1"
style="?android:attr/dropDownItemStyle"
- android:textAppearance="?android:attr/textAppearanceMediumInverse"
+ android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
android:singleLine="true"
android:layout_height="wrap_content"
android:layout_width="0dip"
android:layout_weight="1" />
<ImageView android:id="@android:id/icon2"
- android:layout_width="32dip"
- android:layout_height="32dip"
+ android:layout_width="48dip"
+ android:layout_height="48dip"
android:layout_gravity="center_vertical"
- android:scaleType="fitCenter" />
+ android:scaleType="centerInside" />
</LinearLayout>
diff --git a/core/res/res/layout/search_dropdown_item_icons_2line.xml b/core/res/res/layout/search_dropdown_item_icons_2line.xml
index ad1c905..0d07490 100644
--- a/core/res/res/layout/search_dropdown_item_icons_2line.xml
+++ b/core/res/res/layout/search_dropdown_item_icons_2line.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
-/* //device/apps/common/assets/res/any/layout/simple_spinner_item.xml
+/*
**
** Copyright 2008, The Android Open Source Project
**
@@ -18,56 +18,62 @@
*/
-->
- <!-- NOTE: The appearance of the inner text element must match the appearance -->
- <!-- of the text element in apps/common/res/layout/simple_dropdown_item_2line.xml -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:paddingLeft="4dip"
+ android:paddingRight="2dip"
android:layout_width="fill_parent"
- android:layout_height="?android:attr/listPreferredItemHeight"
- android:orientation="horizontal"
- android:gravity="center_vertical"
- android:baselineAligned="false"
- >
-
- <ImageView android:id="@android:id/icon1"
- android:layout_width="32dip"
- android:layout_height="32dip"
- android:layout_gravity="center_vertical"
- android:scaleType="fitCenter" />
+ android:layout_height="?android:attr/searchResultListItemHeight" >
- <TwoLineListItem
- android:paddingTop="2dip"
- android:paddingBottom="2dip"
- android:layout_width="0dip"
- android:layout_weight="1"
- android:layout_height="wrap_content"
- android:mode="twoLine" >
-
- <TextView
- android:id="@android:id/text1"
- style="?android:attr/dropDownItemStyle"
- android:textAppearance="?android:attr/textAppearanceMediumInverse"
- android:singleLine="true"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content" />
-
- <TextView
- android:id="@android:id/text2"
- style="?android:attr/dropDownItemStyle"
- android:textAppearance="?android:attr/textAppearanceSmallInverse"
- android:textColor="?android:attr/textColorSecondaryInverse"
- android:singleLine="true"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/text1"
- android:layout_alignLeft="@android:id/text1" />
-
- </TwoLineListItem>
+ <!-- Icons come first in the layout, since their placement doesn't depend on
+ the placement of the text views. -->
+ <ImageView android:id="@android:id/icon1"
+ android:layout_width="48dip"
+ android:layout_height="48dip"
+ android:scaleType="centerInside"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone" />
<ImageView android:id="@android:id/icon2"
- android:layout_width="32dip"
- android:layout_height="32dip"
- android:layout_gravity="center_vertical"
- android:scaleType="fitCenter" />
+ android:layout_width="48dip"
+ android:layout_height="48dip"
+ android:scaleType="centerInside"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone" />
-</LinearLayout>
+ <!-- The subtitle comes before the title, since the height of the title depends on whether the
+ subtitle is visible or gone. -->
+ <TextView android:id="@android:id/text2"
+ style="?android:attr/dropDownItemStyle"
+ android:textAppearance="?android:attr/textAppearanceSearchResultSubtitle"
+ android:singleLine="true"
+ android:layout_width="fill_parent"
+ android:layout_height="29dip"
+ android:paddingBottom="4dip"
+ android:gravity="top"
+ android:layout_toRightOf="@android:id/icon1"
+ android:layout_toLeftOf="@android:id/icon2"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_alignParentBottom="true"
+ android:visibility="gone" />
+
+ <!-- The title is placed above the subtitle, if there is one. If there is no
+ subtitle, it fills the parent. -->
+ <TextView android:id="@android:id/text1"
+ style="?android:attr/dropDownItemStyle"
+ android:textAppearance="?android:attr/textAppearanceSearchResultTitle"
+ android:singleLine="true"
+ android:layout_width="fill_parent"
+ android:layout_height="29dip"
+ android:paddingTop="4dip"
+ android:gravity="center_vertical"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@android:id/icon1"
+ android:layout_toLeftOf="@android:id/icon2"
+ android:layout_above="@android:id/text2"
+ android:layout_alignWithParentIfMissing="true" />
+
+</RelativeLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index b98558e..972953b 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -94,6 +94,11 @@
<!-- Text color, typeface, size, and style for "small" inverse text. Defaults to secondary inverse text color. -->
<attr name="textAppearanceSmallInverse" format="reference" />
+ <!-- Text color, typeface, size, and style for system search result title. Defaults to primary inverse text color. @hide -->
+ <attr name="textAppearanceSearchResultTitle" format="reference" />
+ <!-- Text color, typeface, size, and style for system search result subtitle. Defaults to primary inverse text color. @hide -->
+ <attr name="textAppearanceSearchResultSubtitle" format="reference" />
+
<!-- Text color, typeface, size, and style for the text inside of a button. -->
<attr name="textAppearanceButton" format="reference" />
@@ -147,6 +152,8 @@
<!-- The preferred list item height -->
<attr name="listPreferredItemHeight" format="dimension" />
<!-- The drawable for the list divider -->
+ <!-- The list item height for search results. @hide -->
+ <attr name="searchResultListItemHeight" format="dimension" />
<attr name="listDivider" format="reference" />
<!-- TextView style for list separators. -->
<attr name="listSeparatorTextViewStyle" format="reference" />
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index d7b654e..a436f61 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -138,6 +138,7 @@
</style>
<!-- Window animations that are applied to the search bar overlay window.
+ Previously used, but currently unused.
{@hide Pending API council approval} -->
<style name="Animation.SearchBar">
<item name="windowEnterAnimation">@anim/search_bar_enter</item>
@@ -574,6 +575,24 @@
<item name="android:textColor">@android:color/primary_text_light_disable_only</item>
</style>
+ <!-- @hide -->
+ <style name="TextAppearance.SearchResult">
+ <item name="android:textStyle">normal</item>
+ <item name="android:textColor">?textColorPrimaryInverse</item>
+ <item name="android:textColorHint">?textColorHintInverse</item>
+ </style>
+
+ <!-- @hide -->
+ <style name="TextAppearance.SearchResult.Title">
+ <item name="android:textSize">16sp</item>
+ </style>
+
+ <!-- @hide -->
+ <style name="TextAppearance.SearchResult.Subtitle">
+ <item name="android:textSize">13sp</item>
+ <item name="android:textColor">?textColorSecondaryInverse</item>
+ </style>
+
<style name="TextAppearance.WindowTitle">
<item name="android:textColor">#fff</item>
<item name="android:textSize">14sp</item>
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index 6b3d740..dfd2391 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -57,6 +57,12 @@
<item name="textAppearanceLargeInverse">@android:style/TextAppearance.Large.Inverse</item>
<item name="textAppearanceMediumInverse">@android:style/TextAppearance.Medium.Inverse</item>
<item name="textAppearanceSmallInverse">@android:style/TextAppearance.Small.Inverse</item>
+
+ <!-- @hide -->
+ <item name="textAppearanceSearchResultTitle">@android:style/TextAppearance.SearchResult.Title</item>
+
+ <!-- @hide -->
+ <item name="textAppearanceSearchResultSubtitle">@android:style/TextAppearance.SearchResult.Subtitle</item>
<item name="textAppearanceButton">@android:style/TextAppearance.Widget.Button</item>
@@ -75,6 +81,8 @@
<!-- List attributes -->
<item name="listPreferredItemHeight">64dip</item>
+ <!-- @hide -->
+ <item name="searchResultListItemHeight">58dip</item>
<item name="listDivider">@drawable/divider_horizontal_dark</item>
<item name="listSeparatorTextViewStyle">@android:style/Widget.TextView.ListSeparator</item>
@@ -355,7 +363,6 @@
<!-- Theme for the search input bar. -->
<style name="Theme.SearchBar" parent="Theme.Panel">
<item name="android:backgroundDimEnabled">true</item>
- <item name="android:windowAnimationStyle">@android:style/Animation.SearchBar</item>
<item name="windowContentOverlay">@null</item>
</style>
diff --git a/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java b/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java
index 09e3b02..f3c1542 100644
--- a/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java
+++ b/tests/AndroidTests/src/com/android/unit_tests/SearchManagerTest.java
@@ -23,27 +23,11 @@ import android.app.ISearchManager;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.content.res.XmlResourceParser;
import android.os.ServiceManager;
-import android.server.search.SearchableInfo;
-import android.server.search.SearchableInfo.ActionKeyInfo;
-import android.test.ActivityInstrumentationTestCase;
-import android.test.MoreAsserts;
-import android.test.mock.MockContext;
-import android.test.mock.MockPackageManager;
+import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.AndroidRuntimeException;
-import android.view.KeyEvent;
-
-import java.util.ArrayList;
-import java.util.List;
/**
* To launch this test from the command line:
@@ -52,7 +36,7 @@ import java.util.List;
* -e class com.android.unit_tests.SearchManagerTest \
* com.android.unit_tests/android.test.InstrumentationTestRunner
*/
-public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActivity> {
+public class SearchManagerTest extends ActivityInstrumentationTestCase2<LocalActivity> {
// If non-zero, enable a set of tests that start and stop the search manager.
// This is currently disabled because it's causing an unwanted jump from the unit test
@@ -71,18 +55,6 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActi
* testSearchManagerInvocations()
* FIX - make it work again
* stress test with a very long string
- *
- * SearchableInfo tests
- * Mock the context so I can provide very specific input data
- * Confirm OK with "zero" searchables
- * Confirm "good" metadata read properly
- * Confirm "bad" metadata skipped properly
- * Confirm ordering of searchables
- * Confirm "good" actionkeys
- * confirm "bad" actionkeys are rejected
- * confirm XML ordering enforced (will fail today - bug in SearchableInfo)
- * findActionKey works
- * getIcon works
*
* SearchManager tests
* confirm proper identification of "default" activity based on policy, not hardcoded contacts
@@ -195,348 +167,6 @@ public class SearchManagerTest extends ActivityInstrumentationTestCase<LocalActi
searchManager.stopSearch();
}
}
-
- /**
- * The goal of this test is to confirm proper operation of the
- * SearchableInfo helper class.
- *
- * TODO: The metadata source needs to be mocked out because adding
- * searchability metadata via this test is causing it to leak into the
- * real system. So for now I'm just going to test for existence of the
- * GoogleSearch app (which is searchable).
- */
- @LargeTest
- public void testSearchableGoogleSearch() {
- // test basic array & hashmap
- SearchableInfo.buildSearchableList(mContext);
-
- // test linkage from another activity
- // TODO inject this via mocking into the package manager.
- // TODO for now, just check for searchable GoogleSearch app (this isn't really a unit test)
- ComponentName thisActivity = new ComponentName(
- "com.android.googlesearch",
- "com.android.googlesearch.GoogleSearch");
-
- SearchableInfo si = SearchableInfo.getSearchableInfo(mContext, thisActivity);
- assertNotNull(si);
- assertTrue(si.mSearchable);
- assertEquals(thisActivity, si.mSearchActivity);
-
- Context appContext = si.getActivityContext(mContext);
- assertNotNull(appContext);
- MoreAsserts.assertNotEqual(appContext, mContext);
- assertEquals("Google Search", appContext.getString(si.getHintId()));
- assertEquals("Google", appContext.getString(si.getLabelId()));
- }
-
- /**
- * Test that non-searchable activities return no searchable info (this would typically
- * trigger the use of the default searchable e.g. contacts)
- */
- @LargeTest
- public void testNonSearchable() {
- // test basic array & hashmap
- SearchableInfo.buildSearchableList(mContext);
-
- // confirm that we return null for non-searchy activities
- ComponentName nonActivity = new ComponentName(
- "com.android.unit_tests",
- "com.android.unit_tests.NO_SEARCH_ACTIVITY");
- SearchableInfo si = SearchableInfo.getSearchableInfo(mContext, nonActivity);
- assertNull(si);
- }
-
- /**
- * This is an attempt to run the searchable info list with a mocked context. Here are some
- * things I'd like to test.
- *
- * Confirm OK with "zero" searchables
- * Confirm "good" metadata read properly
- * Confirm "bad" metadata skipped properly
- * Confirm ordering of searchables
- * Confirm "good" actionkeys
- * confirm "bad" actionkeys are rejected
- * confirm XML ordering enforced (will fail today - bug in SearchableInfo)
- * findActionKey works
- * getIcon works
- */
- @LargeTest
- public void testSearchableMocked() {
- MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
- MyMockContext mockContext = new MyMockContext(mContext, mockPM);
- ArrayList<SearchableInfo> searchables;
- int count;
-
- // build item list with real-world source data
- mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_PASSTHROUGH);
- SearchableInfo.buildSearchableList(mockContext);
- // tests with "real" searchables (deprecate, this should be a unit test)
- searchables = SearchableInfo.getSearchablesList();
- count = searchables.size();
- assertTrue(count >= 1); // this isn't really a unit test
- checkSearchables(searchables);
-
- // build item list with mocked search data
- // this round of tests confirms good operations with "zero" searchables found
- // This should return either a null pointer or an empty list
- mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_MOCK_ZERO);
- SearchableInfo.buildSearchableList(mockContext);
- searchables = SearchableInfo.getSearchablesList();
- if (searchables != null) {
- count = searchables.size();
- assertTrue(count == 0);
- }
- }
-
- /**
- * Generic health checker for an array of searchables.
- *
- * This is designed to pass for any semi-legal searchable, without knowing much about
- * the format of the underlying data. It's fairly easy for a non-compliant application
- * to provide meta-data that will pass here (e.g. a non-existent suggestions authority).
- *
- * @param searchables The list of searchables to examine.
- */
- private void checkSearchables(ArrayList<SearchableInfo> searchablesList) {
- assertNotNull(searchablesList);
- int count = searchablesList.size();
- for (int ii = 0; ii < count; ii++) {
- SearchableInfo si = searchablesList.get(ii);
- assertNotNull(si);
- assertTrue(si.mSearchable);
- assertTrue(si.getLabelId() != 0); // This must be a useable string
- assertNotEmpty(si.mSearchActivity.getClassName());
- assertNotEmpty(si.mSearchActivity.getPackageName());
- if (si.getSuggestAuthority() != null) {
- // The suggestion fields are largely optional, so we'll just confirm basic health
- assertNotEmpty(si.getSuggestAuthority());
- assertNullOrNotEmpty(si.getSuggestPath());
- assertNullOrNotEmpty(si.getSuggestSelection());
- assertNullOrNotEmpty(si.getSuggestIntentAction());
- assertNullOrNotEmpty(si.getSuggestIntentData());
- }
- /* Add a way to get the entire action key list, then explicitly test its elements */
- /* For now, test the most common action key (CALL) */
- ActionKeyInfo ai = si.findActionKey(KeyEvent.KEYCODE_CALL);
- if (ai != null) {
- assertEquals(ai.mKeyCode, KeyEvent.KEYCODE_CALL);
- // one of these three fields must be non-null & non-empty
- boolean m1 = (ai.mQueryActionMsg != null) && (ai.mQueryActionMsg.length() > 0);
- boolean m2 = (ai.mSuggestActionMsg != null) && (ai.mSuggestActionMsg.length() > 0);
- boolean m3 = (ai.mSuggestActionMsgColumn != null) &&
- (ai.mSuggestActionMsgColumn.length() > 0);
- assertTrue(m1 || m2 || m3);
- }
-
- /*
- * Find ways to test these:
- *
- * private int mSearchMode
- * private Drawable mIcon
- */
-
- /*
- * Explicitly not tested here:
- *
- * Can be null, so not much to see:
- * public String mSearchHint
- * private String mZeroQueryBanner
- *
- * To be deprecated/removed, so don't bother:
- * public boolean mFilterMode
- * public boolean mQuickStart
- * private boolean mIconResized
- * private int mIconResizeWidth
- * private int mIconResizeHeight
- *
- * All of these are "internal" working variables, not part of any contract
- * private ActivityInfo mActivityInfo
- * private Rect mTempRect
- * private String mSuggestProviderPackage
- * private String mCacheActivityContext
- */
- }
- }
-
- /**
- * Combo assert for "string not null and not empty"
- */
- private void assertNotEmpty(final String s) {
- assertNotNull(s);
- MoreAsserts.assertNotEqual(s, "");
- }
-
- /**
- * Combo assert for "string null or (not null and not empty)"
- */
- private void assertNullOrNotEmpty(final String s) {
- if (s != null) {
- MoreAsserts.assertNotEqual(s, "");
- }
- }
-
- /**
- * This is a mock for context. Used to perform a true unit test on SearchableInfo.
- *
- */
- private class MyMockContext extends MockContext {
-
- protected Context mRealContext;
- protected PackageManager mPackageManager;
-
- /**
- * Constructor.
- *
- * @param realContext Please pass in a real context for some pass-throughs to function.
- */
- MyMockContext(Context realContext, PackageManager packageManager) {
- mRealContext = realContext;
- mPackageManager = packageManager;
- }
-
- /**
- * Resources. Pass through for now.
- */
- @Override
- public Resources getResources() {
- return mRealContext.getResources();
- }
-
- /**
- * Package manager. Pass through for now.
- */
- @Override
- public PackageManager getPackageManager() {
- return mPackageManager;
- }
-
- /**
- * Package manager. Pass through for now.
- */
- @Override
- public Context createPackageContext(String packageName, int flags)
- throws PackageManager.NameNotFoundException {
- return mRealContext.createPackageContext(packageName, flags);
- }
- }
-
-/**
- * This is a mock for package manager. Used to perform a true unit test on SearchableInfo.
- *
- */
- private class MyMockPackageManager extends MockPackageManager {
-
- public final static int SEARCHABLES_PASSTHROUGH = 0;
- public final static int SEARCHABLES_MOCK_ZERO = 1;
- public final static int SEARCHABLES_MOCK_ONEGOOD = 2;
- public final static int SEARCHABLES_MOCK_ONEGOOD_ONEBAD = 3;
-
- protected PackageManager mRealPackageManager;
- protected int mSearchablesMode;
-
- public MyMockPackageManager(PackageManager realPM) {
- mRealPackageManager = realPM;
- mSearchablesMode = SEARCHABLES_PASSTHROUGH;
- }
-
- /**
- * Set the mode for various tests.
- */
- public void setSearchablesMode(int newMode) {
- switch (newMode) {
- case SEARCHABLES_PASSTHROUGH:
- case SEARCHABLES_MOCK_ZERO:
- mSearchablesMode = newMode;
- break;
-
- default:
- throw new UnsupportedOperationException();
- }
- }
-
- /**
- * Find activities that support a given intent.
- *
- * Retrieve all activities that can be performed for the given intent.
- *
- * @param intent The desired intent as per resolveActivity().
- * @param flags Additional option flags. The most important is
- * MATCH_DEFAULT_ONLY, to limit the resolution to only
- * those activities that support the CATEGORY_DEFAULT.
- *
- * @return A List<ResolveInfo> containing one entry for each matching
- * Activity. These are ordered from best to worst match -- that
- * is, the first item in the list is what is returned by
- * resolveActivity(). If there are no matching activities, an empty
- * list is returned.
- */
- @Override
- public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
- assertNotNull(intent);
- assertEquals(intent.getAction(), Intent.ACTION_SEARCH);
- switch (mSearchablesMode) {
- case SEARCHABLES_PASSTHROUGH:
- return mRealPackageManager.queryIntentActivities(intent, flags);
- case SEARCHABLES_MOCK_ZERO:
- return null;
- default:
- throw new UnsupportedOperationException();
- }
- }
-
- /**
- * Retrieve an XML file from a package. This is a low-level API used to
- * retrieve XML meta data.
- *
- * @param packageName The name of the package that this xml is coming from.
- * Can not be null.
- * @param resid The resource identifier of the desired xml. Can not be 0.
- * @param appInfo Overall information about <var>packageName</var>. This
- * may be null, in which case the application information will be retrieved
- * for you if needed; if you already have this information around, it can
- * be much more efficient to supply it here.
- *
- * @return Returns an XmlPullParser allowing you to parse out the XML
- * data. Returns null if the xml resource could not be found for any
- * reason.
- */
- @Override
- public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) {
- assertNotNull(packageName);
- MoreAsserts.assertNotEqual(packageName, "");
- MoreAsserts.assertNotEqual(resid, 0);
- switch (mSearchablesMode) {
- case SEARCHABLES_PASSTHROUGH:
- return mRealPackageManager.getXml(packageName, resid, appInfo);
- case SEARCHABLES_MOCK_ZERO:
- default:
- throw new UnsupportedOperationException();
- }
- }
-
- /**
- * Find a single content provider by its base path name.
- *
- * @param name The name of the provider to find.
- * @param flags Additional option flags. Currently should always be 0.
- *
- * @return ContentProviderInfo Information about the provider, if found,
- * else null.
- */
- @Override
- public ProviderInfo resolveContentProvider(String name, int flags) {
- assertNotNull(name);
- MoreAsserts.assertNotEqual(name, "");
- assertEquals(flags, 0);
- switch (mSearchablesMode) {
- case SEARCHABLES_PASSTHROUGH:
- return mRealPackageManager.resolveContentProvider(name, flags);
- case SEARCHABLES_MOCK_ZERO:
- default:
- throw new UnsupportedOperationException();
- }
- }
- }
}
diff --git a/tests/AndroidTests/src/com/android/unit_tests/SearchablesTest.java b/tests/AndroidTests/src/com/android/unit_tests/SearchablesTest.java
new file mode 100644
index 0000000..c299b10
--- /dev/null
+++ b/tests/AndroidTests/src/com/android/unit_tests/SearchablesTest.java
@@ -0,0 +1,411 @@
+/*
+ * 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 com.android.unit_tests;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.server.search.SearchableInfo;
+import android.server.search.Searchables;
+import android.server.search.SearchableInfo.ActionKeyInfo;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.mock.MockContext;
+import android.test.mock.MockPackageManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.KeyEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * To launch this test from the command line:
+ *
+ * adb shell am instrument -w \
+ * -e class com.android.unit_tests.SearchablesTest \
+ * com.android.unit_tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class SearchablesTest extends AndroidTestCase {
+
+ /*
+ * SearchableInfo tests
+ * Mock the context so I can provide very specific input data
+ * Confirm OK with "zero" searchables
+ * Confirm "good" metadata read properly
+ * Confirm "bad" metadata skipped properly
+ * Confirm ordering of searchables
+ * Confirm "good" actionkeys
+ * confirm "bad" actionkeys are rejected
+ * confirm XML ordering enforced (will fail today - bug in SearchableInfo)
+ * findActionKey works
+ * getIcon works
+ */
+
+ /**
+ * The goal of this test is to confirm proper operation of the
+ * SearchableInfo helper class.
+ *
+ * TODO: The metadata source needs to be mocked out because adding
+ * searchability metadata via this test is causing it to leak into the
+ * real system. So for now I'm just going to test for existence of the
+ * GoogleSearch app (which is searchable).
+ */
+ public void testSearchableGoogleSearch() {
+ // test basic array & hashmap
+ Searchables searchables = new Searchables(mContext);
+ searchables.buildSearchableList();
+
+ // test linkage from another activity
+ // TODO inject this via mocking into the package manager.
+ // TODO for now, just check for searchable GoogleSearch app (this isn't really a unit test)
+ ComponentName thisActivity = new ComponentName(
+ "com.android.googlesearch",
+ "com.android.googlesearch.GoogleSearch");
+
+ SearchableInfo si = searchables.getSearchableInfo(thisActivity);
+ assertNotNull(si);
+ assertTrue(si.mSearchable);
+ assertEquals(thisActivity, si.mSearchActivity);
+
+ Context appContext = si.getActivityContext(mContext);
+ assertNotNull(appContext);
+ MoreAsserts.assertNotEqual(appContext, mContext);
+ assertEquals("Google Search", appContext.getString(si.getHintId()));
+ assertEquals("Google", appContext.getString(si.getLabelId()));
+ }
+
+ /**
+ * Test that non-searchable activities return no searchable info (this would typically
+ * trigger the use of the default searchable e.g. contacts)
+ */
+ public void testNonSearchable() {
+ // test basic array & hashmap
+ Searchables searchables = new Searchables(mContext);
+ searchables.buildSearchableList();
+
+ // confirm that we return null for non-searchy activities
+ ComponentName nonActivity = new ComponentName(
+ "com.android.unit_tests",
+ "com.android.unit_tests.NO_SEARCH_ACTIVITY");
+ SearchableInfo si = searchables.getSearchableInfo(nonActivity);
+ assertNull(si);
+ }
+
+ /**
+ * This is an attempt to run the searchable info list with a mocked context. Here are some
+ * things I'd like to test.
+ *
+ * Confirm OK with "zero" searchables
+ * Confirm "good" metadata read properly
+ * Confirm "bad" metadata skipped properly
+ * Confirm ordering of searchables
+ * Confirm "good" actionkeys
+ * confirm "bad" actionkeys are rejected
+ * confirm XML ordering enforced (will fail today - bug in SearchableInfo)
+ * findActionKey works
+ * getIcon works
+
+ */
+ public void testSearchableMocked() {
+ MyMockPackageManager mockPM = new MyMockPackageManager(mContext.getPackageManager());
+ MyMockContext mockContext = new MyMockContext(mContext, mockPM);
+ Searchables searchables;
+ ArrayList<SearchableInfo> searchablesList;
+ int count;
+
+
+ // build item list with real-world source data
+ mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_PASSTHROUGH);
+ searchables = new Searchables(mockContext);
+ searchables.buildSearchableList();
+ // tests with "real" searchables (deprecate, this should be a unit test)
+ searchablesList = searchables.getSearchablesList();
+ count = searchablesList.size();
+ assertTrue(count >= 1); // this isn't really a unit test
+ checkSearchables(searchablesList);
+
+ // build item list with mocked search data
+ // this round of tests confirms good operations with "zero" searchables found
+ // This should return either a null pointer or an empty list
+ mockPM.setSearchablesMode(MyMockPackageManager.SEARCHABLES_MOCK_ZERO);
+ searchables = new Searchables(mockContext);
+ searchables.buildSearchableList();
+ searchablesList = searchables.getSearchablesList();
+ if (searchablesList != null) {
+ count = searchablesList.size();
+ assertTrue(count == 0);
+ }
+ }
+
+ /**
+ * Generic health checker for an array of searchables.
+ *
+ * This is designed to pass for any semi-legal searchable, without knowing much about
+ * the format of the underlying data. It's fairly easy for a non-compliant application
+ * to provide meta-data that will pass here (e.g. a non-existent suggestions authority).
+ *
+ * @param searchables The list of searchables to examine.
+ */
+ private void checkSearchables(ArrayList<SearchableInfo> searchablesList) {
+ assertNotNull(searchablesList);
+ int count = searchablesList.size();
+ for (int ii = 0; ii < count; ii++) {
+ SearchableInfo si = searchablesList.get(ii);
+ assertNotNull(si);
+ assertTrue(si.mSearchable);
+ assertTrue(si.getLabelId() != 0); // This must be a useable string
+ assertNotEmpty(si.mSearchActivity.getClassName());
+ assertNotEmpty(si.mSearchActivity.getPackageName());
+ if (si.getSuggestAuthority() != null) {
+ // The suggestion fields are largely optional, so we'll just confirm basic health
+ assertNotEmpty(si.getSuggestAuthority());
+ assertNullOrNotEmpty(si.getSuggestPath());
+ assertNullOrNotEmpty(si.getSuggestSelection());
+ assertNullOrNotEmpty(si.getSuggestIntentAction());
+ assertNullOrNotEmpty(si.getSuggestIntentData());
+ }
+ /* Add a way to get the entire action key list, then explicitly test its elements */
+ /* For now, test the most common action key (CALL) */
+ ActionKeyInfo ai = si.findActionKey(KeyEvent.KEYCODE_CALL);
+ if (ai != null) {
+ assertEquals(ai.mKeyCode, KeyEvent.KEYCODE_CALL);
+ // one of these three fields must be non-null & non-empty
+ boolean m1 = (ai.mQueryActionMsg != null) && (ai.mQueryActionMsg.length() > 0);
+ boolean m2 = (ai.mSuggestActionMsg != null) && (ai.mSuggestActionMsg.length() > 0);
+ boolean m3 = (ai.mSuggestActionMsgColumn != null) &&
+ (ai.mSuggestActionMsgColumn.length() > 0);
+ assertTrue(m1 || m2 || m3);
+ }
+
+ /*
+ * Find ways to test these:
+ *
+ * private int mSearchMode
+ * private Drawable mIcon
+ */
+
+ /*
+ * Explicitly not tested here:
+ *
+ * Can be null, so not much to see:
+ * public String mSearchHint
+ * private String mZeroQueryBanner
+ *
+ * To be deprecated/removed, so don't bother:
+ * public boolean mFilterMode
+ * public boolean mQuickStart
+ * private boolean mIconResized
+ * private int mIconResizeWidth
+ * private int mIconResizeHeight
+ *
+ * All of these are "internal" working variables, not part of any contract
+ * private ActivityInfo mActivityInfo
+ * private Rect mTempRect
+ * private String mSuggestProviderPackage
+ * private String mCacheActivityContext
+ */
+ }
+ }
+
+ /**
+ * Combo assert for "string not null and not empty"
+ */
+ private void assertNotEmpty(final String s) {
+ assertNotNull(s);
+ MoreAsserts.assertNotEqual(s, "");
+ }
+
+ /**
+ * Combo assert for "string null or (not null and not empty)"
+ */
+ private void assertNullOrNotEmpty(final String s) {
+ if (s != null) {
+ MoreAsserts.assertNotEqual(s, "");
+ }
+ }
+
+ /**
+ * This is a mock for context. Used to perform a true unit test on SearchableInfo.
+ *
+ */
+ private class MyMockContext extends MockContext {
+
+ protected Context mRealContext;
+ protected PackageManager mPackageManager;
+
+ /**
+ * Constructor.
+ *
+ * @param realContext Please pass in a real context for some pass-throughs to function.
+ */
+ MyMockContext(Context realContext, PackageManager packageManager) {
+ mRealContext = realContext;
+ mPackageManager = packageManager;
+ }
+
+ /**
+ * Resources. Pass through for now.
+ */
+ @Override
+ public Resources getResources() {
+ return mRealContext.getResources();
+ }
+
+ /**
+ * Package manager. Pass through for now.
+ */
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ /**
+ * Package manager. Pass through for now.
+ */
+ @Override
+ public Context createPackageContext(String packageName, int flags)
+ throws PackageManager.NameNotFoundException {
+ return mRealContext.createPackageContext(packageName, flags);
+ }
+ }
+
+/**
+ * This is a mock for package manager. Used to perform a true unit test on SearchableInfo.
+ *
+ */
+ private class MyMockPackageManager extends MockPackageManager {
+
+ public final static int SEARCHABLES_PASSTHROUGH = 0;
+ public final static int SEARCHABLES_MOCK_ZERO = 1;
+ public final static int SEARCHABLES_MOCK_ONEGOOD = 2;
+ public final static int SEARCHABLES_MOCK_ONEGOOD_ONEBAD = 3;
+
+ protected PackageManager mRealPackageManager;
+ protected int mSearchablesMode;
+
+ public MyMockPackageManager(PackageManager realPM) {
+ mRealPackageManager = realPM;
+ mSearchablesMode = SEARCHABLES_PASSTHROUGH;
+ }
+
+ /**
+ * Set the mode for various tests.
+ */
+ public void setSearchablesMode(int newMode) {
+ switch (newMode) {
+ case SEARCHABLES_PASSTHROUGH:
+ case SEARCHABLES_MOCK_ZERO:
+ mSearchablesMode = newMode;
+ break;
+
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Find activities that support a given intent.
+ *
+ * Retrieve all activities that can be performed for the given intent.
+ *
+ * @param intent The desired intent as per resolveActivity().
+ * @param flags Additional option flags. The most important is
+ * MATCH_DEFAULT_ONLY, to limit the resolution to only
+ * those activities that support the CATEGORY_DEFAULT.
+ *
+ * @return A List<ResolveInfo> containing one entry for each matching
+ * Activity. These are ordered from best to worst match -- that
+ * is, the first item in the list is what is returned by
+ * resolveActivity(). If there are no matching activities, an empty
+ * list is returned.
+ */
+ @Override
+ public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
+ assertNotNull(intent);
+ assertEquals(intent.getAction(), Intent.ACTION_SEARCH);
+ switch (mSearchablesMode) {
+ case SEARCHABLES_PASSTHROUGH:
+ return mRealPackageManager.queryIntentActivities(intent, flags);
+ case SEARCHABLES_MOCK_ZERO:
+ return null;
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Retrieve an XML file from a package. This is a low-level API used to
+ * retrieve XML meta data.
+ *
+ * @param packageName The name of the package that this xml is coming from.
+ * Can not be null.
+ * @param resid The resource identifier of the desired xml. Can not be 0.
+ * @param appInfo Overall information about <var>packageName</var>. This
+ * may be null, in which case the application information will be retrieved
+ * for you if needed; if you already have this information around, it can
+ * be much more efficient to supply it here.
+ *
+ * @return Returns an XmlPullParser allowing you to parse out the XML
+ * data. Returns null if the xml resource could not be found for any
+ * reason.
+ */
+ @Override
+ public XmlResourceParser getXml(String packageName, int resid, ApplicationInfo appInfo) {
+ assertNotNull(packageName);
+ MoreAsserts.assertNotEqual(packageName, "");
+ MoreAsserts.assertNotEqual(resid, 0);
+ switch (mSearchablesMode) {
+ case SEARCHABLES_PASSTHROUGH:
+ return mRealPackageManager.getXml(packageName, resid, appInfo);
+ case SEARCHABLES_MOCK_ZERO:
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Find a single content provider by its base path name.
+ *
+ * @param name The name of the provider to find.
+ * @param flags Additional option flags. Currently should always be 0.
+ *
+ * @return ContentProviderInfo Information about the provider, if found,
+ * else null.
+ */
+ @Override
+ public ProviderInfo resolveContentProvider(String name, int flags) {
+ assertNotNull(name);
+ MoreAsserts.assertNotEqual(name, "");
+ assertEquals(flags, 0);
+ switch (mSearchablesMode) {
+ case SEARCHABLES_PASSTHROUGH:
+ return mRealPackageManager.resolveContentProvider(name, flags);
+ case SEARCHABLES_MOCK_ZERO:
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+}
+
diff --git a/tests/FrameworkTest/tests/src/android/widget/AutoCompleteTextViewPopup.java b/tests/FrameworkTest/tests/src/android/widget/AutoCompleteTextViewPopup.java
index 663b7a4..6f89fce 100644
--- a/tests/FrameworkTest/tests/src/android/widget/AutoCompleteTextViewPopup.java
+++ b/tests/FrameworkTest/tests/src/android/widget/AutoCompleteTextViewPopup.java
@@ -147,4 +147,56 @@ public class AutoCompleteTextViewPopup
// now try moving "down" - nothing should happen since there's no longer an adapter
sendKeys("DPAD_DOWN");
}
+
+ /** Test the show/hide behavior of the drop-down. */
+ @MediumTest
+ public void testPopupShow() throws Throwable {
+ AutoCompleteTextViewSimple theActivity = getActivity();
+ final AutoCompleteTextView textView = theActivity.getTextView();
+ final Instrumentation instrumentation = getInstrumentation();
+
+ // Drop-down should not be showing when no text has been entered
+ assertFalse("isPopupShowing() on start", textView.isPopupShowing());
+
+ // focus and type
+ textView.requestFocus();
+ instrumentation.waitForIdleSync();
+ sendKeys("A");
+
+ // Drop-down should now be visible
+ assertTrue("isPopupShowing() after typing", textView.isPopupShowing());
+
+ // Clear the text
+ runTestOnUiThread(new Runnable() {
+ public void run() {
+ textView.setText("");
+ }
+ });
+ instrumentation.waitForIdleSync();
+
+ // Drop-down should be hidden when text is cleared
+ assertFalse("isPopupShowing() after text cleared", textView.isPopupShowing());
+
+ // Set the text, without filtering
+ runTestOnUiThread(new Runnable() {
+ public void run() {
+ textView.setText("a", false);
+ }
+ });
+ instrumentation.waitForIdleSync();
+
+ // Drop-down should still be hidden
+ assertFalse("isPopupShowing() after setText(\"a\", false)", textView.isPopupShowing());
+
+ // Set the text, now with filtering
+ runTestOnUiThread(new Runnable() {
+ public void run() {
+ textView.setText("a");
+ }
+ });
+ instrumentation.waitForIdleSync();
+
+ // Drop-down should show up after setText() with filtering
+ assertTrue("isPopupShowing() after text set", textView.isPopupShowing());
+ }
}