diff options
author | Karl Rosaen <> | 2009-04-23 19:00:21 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-04-23 19:00:21 -0700 |
commit | 875d50a4b9294b2be33cff6493cae7acd1d07ea7 (patch) | |
tree | 48cc044c4719e53d214e5fa6c273d1ecd9078356 | |
parent | b08971b876801d9cb878f3f0ca0ebfde7c9bea8e (diff) | |
download | frameworks_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
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 Binary files differnew file mode 100644 index 0000000..9b7d3e5 --- /dev/null +++ b/core/res/res/drawable/btn_global_search_normal.9.png 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 Binary files differnew file mode 100644 index 0000000..ec39178 --- /dev/null +++ b/core/res/res/drawable/btn_search_dialog_default.9.png diff --git a/core/res/res/drawable/btn_search_dialog_pressed.9.png b/core/res/res/drawable/btn_search_dialog_pressed.9.png Binary files differnew file mode 100644 index 0000000..5f52fef --- /dev/null +++ b/core/res/res/drawable/btn_search_dialog_pressed.9.png diff --git a/core/res/res/drawable/btn_search_dialog_selected.9.png b/core/res/res/drawable/btn_search_dialog_selected.9.png Binary files differnew file mode 100644 index 0000000..9fc2fde --- /dev/null +++ b/core/res/res/drawable/btn_search_dialog_selected.9.png 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 Binary files differnew file mode 100644 index 0000000..2a3366c --- /dev/null +++ b/core/res/res/drawable/btn_search_dialog_voice_default.9.png 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 Binary files differnew file mode 100644 index 0000000..57d7a74 --- /dev/null +++ b/core/res/res/drawable/btn_search_dialog_voice_pressed.9.png 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 Binary files differnew file mode 100644 index 0000000..db3187e --- /dev/null +++ b/core/res/res/drawable/btn_search_dialog_voice_selected.9.png diff --git a/core/res/res/drawable/search_dropdown_background.9.png b/core/res/res/drawable/search_dropdown_background.9.png Binary files differnew file mode 100755 index 0000000..a6923b7 --- /dev/null +++ b/core/res/res/drawable/search_dropdown_background.9.png diff --git a/core/res/res/drawable/search_plate_global.9.png b/core/res/res/drawable/search_plate_global.9.png Binary files differnew file mode 100644 index 0000000..126054b --- /dev/null +++ b/core/res/res/drawable/search_plate_global.9.png 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 Binary files differnew file mode 100755 index 0000000..7dc5b27 --- /dev/null +++ b/core/res/res/drawable/textfield_search_default.9.png diff --git a/core/res/res/drawable/textfield_search_pressed.9.png b/core/res/res/drawable/textfield_search_pressed.9.png Binary files differnew file mode 100644 index 0000000..da00c25 --- /dev/null +++ b/core/res/res/drawable/textfield_search_pressed.9.png diff --git a/core/res/res/drawable/textfield_search_selected.9.png b/core/res/res/drawable/textfield_search_selected.9.png Binary files differnew file mode 100755 index 0000000..a9fd3b2 --- /dev/null +++ b/core/res/res/drawable/textfield_search_selected.9.png 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()); + } } |