diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-04-29 13:34:51 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-04-29 13:34:51 -0700 |
commit | 8f080ec4292be02fad9896260bbd3cf5461f9399 (patch) | |
tree | 7b230c94b8af191c2ddc13e20de78478a2964b3b /core/java | |
parent | 9f5a54a59dac6df0ffe2d19c71733b4a0c7b00c2 (diff) | |
parent | f3ccf8a5a5a3f6e46781538358bddca992a70e3d (diff) | |
download | frameworks_base-8f080ec4292be02fad9896260bbd3cf5461f9399.zip frameworks_base-8f080ec4292be02fad9896260bbd3cf5461f9399.tar.gz frameworks_base-8f080ec4292be02fad9896260bbd3cf5461f9399.tar.bz2 |
am f3ccf8a: Merge branch \'readonly-p4-donut\' into donut
Diffstat (limited to 'core/java')
-rw-r--r-- | core/java/android/app/SearchDialog.java | 1514 | ||||
-rw-r--r-- | core/java/android/app/SearchManager.java | 224 | ||||
-rw-r--r-- | core/java/android/app/SuggestionsAdapter.java | 344 | ||||
-rw-r--r-- | core/java/android/provider/Applications.java | 82 | ||||
-rw-r--r-- | core/java/android/server/search/SearchManagerService.java | 49 | ||||
-rw-r--r-- | core/java/android/server/search/SearchableInfo.java | 362 | ||||
-rw-r--r-- | core/java/android/server/search/Searchables.java | 243 | ||||
-rw-r--r-- | core/java/android/widget/AbsListView.java | 12 | ||||
-rw-r--r-- | core/java/android/widget/AutoCompleteTextView.java | 230 |
9 files changed, 1883 insertions, 1177 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 a17c78d..89faa95 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; @@ -255,6 +259,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mDropDownAnchorId = id; mDropDownAnchorView = null; } +<<<<<<< HEAD:core/java/android/widget/AutoCompleteTextView.java /** * <p>Gets the background of the auto-complete drop-down list.</p> @@ -325,6 +330,169 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return mDropDownHorizontalOffset; } +||||||| + +======= + + /** + * <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); + } + +>>>>>>> f3ccf8a5a5a3f6e46781538358bddca992a70e3d:core/java/android/widget/AutoCompleteTextView.java + /** + * <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> @@ -705,16 +873,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 @@ -786,7 +944,9 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } } - dismissDropDown(); + if (mDropDownDismissedOnCompletion) { + dismissDropDown(); + } } /** @@ -798,6 +958,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> @@ -811,6 +1007,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe Selection.setSelection(spannable, spannable.length()); } + /** {@inheritDoc} */ public void onFilterComplete(int count) { if (mAttachCount <= 0) return; @@ -821,7 +1018,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * to filter. */ - if (count > 0 && enoughToFilter()) { + if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) { if (hasFocus() && hasWindowFocus()) { showDropDown(); } @@ -917,10 +1114,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 { @@ -1027,8 +1223,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) { |