diff options
author | Amith Yamasani <yamasani@google.com> | 2010-12-03 11:22:48 -0800 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2010-12-03 11:22:48 -0800 |
commit | 3b7fec8d56e0634d4c7795258f03023f4885f723 (patch) | |
tree | 29fdf5527041d1d3fc8d6428471b177a9d38af3a | |
parent | c99821d01ab0c1cee54ccb0556e621dfe2ddb55f (diff) | |
parent | 968ec938399033d280b1648123104ac567f2a093 (diff) | |
download | frameworks_base-3b7fec8d56e0634d4c7795258f03023f4885f723.zip frameworks_base-3b7fec8d56e0634d4c7795258f03023f4885f723.tar.gz frameworks_base-3b7fec8d56e0634d4c7795258f03023f4885f723.tar.bz2 |
Merge "Use SearchView in SearchDialog for legacy apps using the old model."
-rw-r--r-- | core/java/android/app/SearchDialog.java | 1018 | ||||
-rw-r--r-- | core/java/android/app/SuggestionsAdapter.java | 653 | ||||
-rw-r--r-- | core/java/android/widget/SearchView.java | 288 | ||||
-rw-r--r-- | core/res/res/layout/search_bar.xml | 95 | ||||
-rw-r--r-- | core/res/res/layout/search_view.xml | 2 | ||||
-rw-r--r-- | core/res/res/values/themes.xml | 17 |
6 files changed, 405 insertions, 1668 deletions
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 0c280f9..4d19b62 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -17,12 +17,6 @@ package android.app; -import static android.app.SuggestionsAdapter.getColumnString; - -import java.util.WeakHashMap; -import java.util.concurrent.atomic.AtomicLong; - -import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -30,21 +24,15 @@ 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.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; -import android.provider.Browser; import android.speech.RecognizerIntent; -import android.text.Editable; import android.text.InputType; import android.text.TextUtils; -import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; @@ -55,18 +43,15 @@ import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; import android.widget.AutoCompleteTextView; -import android.widget.Button; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ListView; +import android.widget.SearchView; import android.widget.TextView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemSelectedListener; + +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicLong; /** * Search dialog. This is controlled by the @@ -74,32 +59,28 @@ import android.widget.AdapterView.OnItemSelectedListener; * * @hide */ -public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener { +public class SearchDialog extends Dialog { // Debugging support private static final boolean DBG = false; private static final String LOG_TAG = "SearchDialog"; - private static final boolean DBG_LOG_TIMING = false; private static final String INSTANCE_KEY_COMPONENT = "comp"; private static final String INSTANCE_KEY_APPDATA = "data"; - private static final String INSTANCE_KEY_STORED_APPDATA = "sData"; private static final String INSTANCE_KEY_USER_QUERY = "uQry"; - + // The string used for privateImeOptions to identify to the IME that it should not show // a microphone button since one already exists in the search dialog. private static final String IME_OPTION_NO_MICROPHONE = "nm"; - private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12; private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; // views & widgets private TextView mBadgeLabel; private ImageView mAppIcon; - private SearchAutoComplete mSearchAutoComplete; - private Button mGoButton; - private ImageButton mVoiceButton; + private AutoCompleteTextView mSearchAutoComplete; private View mSearchPlate; + private SearchView mSearchView; private Drawable mWorkingSpinner; // interaction with searchable application @@ -107,30 +88,15 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private ComponentName mLaunchComponent; private Bundle mAppSearchData; private Context mActivityContext; - private SearchManager mSearchManager; // For voice searching private final Intent mVoiceWebSearchIntent; private final Intent mVoiceAppSearchIntent; - // support for AutoCompleteTextView suggestions display - private SuggestionsAdapter mSuggestionsAdapter; - - // Whether to rewrite queries when selecting suggestions - private static final boolean REWRITE_QUERIES = true; - // 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; - // The query passed in when opening the SearchDialog. Used in the browser - // case to determine whether the user has edited the query. - private String mInitialQuery; - - // 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.ConstantState> mOutsideDrawablesCache = - new WeakHashMap<String, Drawable.ConstantState>(); // Last known IME options value for the search edit text. private int mSearchAutoCompleteImeOptions; @@ -160,7 +126,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mSearchManager = searchManager; } /** @@ -196,30 +161,24 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // get the view elements for local access SearchBar searchBar = (SearchBar) findViewById(com.android.internal.R.id.search_bar); searchBar.setSearchDialog(this); - - mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge); - mSearchAutoComplete = (SearchAutoComplete) - findViewById(com.android.internal.R.id.search_src_text); + mSearchView = (SearchView) findViewById(com.android.internal.R.id.search_view); + mSearchView.setSubmitButtonEnabled(true); + mSearchView.setOnCloseListener(mOnCloseListener); + mSearchView.setOnQueryChangeListener(mOnQueryChangeListener); + mSearchView.setOnSuggestionSelectionListener(mOnSuggestionSelectionListener); + + // TODO: Move the badge logic to SearchView or move the badge to search_bar.xml + mBadgeLabel = (TextView) mSearchView.findViewById(com.android.internal.R.id.search_badge); + mSearchAutoComplete = (AutoCompleteTextView) + mSearchView.findViewById(com.android.internal.R.id.search_src_text); mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon); - 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); + mSearchPlate = mSearchView.findViewById(com.android.internal.R.id.search_plate); mWorkingSpinner = getContext().getResources(). getDrawable(com.android.internal.R.drawable.search_spinner); mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( null, null, mWorkingSpinner, null); setWorking(false); - // attach listeners - mSearchAutoComplete.addTextChangedListener(mTextWatcher); - mSearchAutoComplete.setOnKeyListener(mTextKeyListener); - mSearchAutoComplete.setOnItemClickListener(this); - mSearchAutoComplete.setOnItemSelectedListener(this); - mGoButton.setOnClickListener(mGoButtonClickListener); - mGoButton.setOnKeyListener(mButtonsKeyListener); - mVoiceButton.setOnClickListener(mVoiceButtonClickListener); - mVoiceButton.setOnKeyListener(mButtonsKeyListener); - // pre-hide all the extraneous elements mBadgeLabel.setVisibility(View.GONE); @@ -256,7 +215,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return false; } - mInitialQuery = initialQuery == null ? "" : initialQuery; // finally, load the user's initial text (which may trigger suggestions) setUserQuery(initialQuery); if (selectInitialQuery) { @@ -296,6 +254,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // Recreate the search bar view every time the dialog is shown, to get rid // of any bad state in the AutoCompleteTextView etc createContentView(); + mSearchView.setSearchableInfo(mSearchable); show(); } @@ -326,14 +285,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS getContext().unregisterReceiver(mConfChangeListener); - closeSuggestionsAdapter(); - // dump extra memory we're hanging on to mLaunchComponent = null; mAppSearchData = null; mSearchable = null; mUserQuery = null; - mInitialQuery = null; } /** @@ -349,20 +305,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * 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.close(); - } - mSuggestionsAdapter = null; - } - - /** * Save the minimal set of data necessary to recreate the search * * @return A bundle with the state of the dialog, or {@code null} if the search @@ -385,8 +327,6 @@ 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()}. */ @@ -404,21 +344,18 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return; } } - + /** * Called after resources have changed, e.g. after screen rotation or locale change. */ public void onConfigurationChanged() { if (mSearchable != null && isShowing()) { // Redraw (resources may have changed) - updateSearchButton(); updateSearchAppIcon(); updateSearchBadge(); - updateQueryHint(); if (isLandscapeMode(getContext())) { mSearchAutoComplete.ensureImeVisible(true); } - mSearchAutoComplete.showDropDownAfterLayout(); } } @@ -434,11 +371,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (mSearchable != null) { mDecor.setVisibility(View.VISIBLE); updateSearchAutoComplete(); - updateSearchButton(); updateSearchAppIcon(); updateSearchBadge(); - updateQueryHint(); - updateVoiceButton(TextUtils.isEmpty(mUserQuery)); // In order to properly configure the input method (if one is being used), we // need to let it know if we'll be providing suggestions. Although it would be @@ -474,68 +408,26 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * Updates the auto-complete text view. */ private void updateSearchAutoComplete() { - // close any existing suggestions adapter - closeSuggestionsAdapter(); - - mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation - mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold()); // we dismiss the entire dialog instead mSearchAutoComplete.setDropDownDismissedOnCompletion(false); - - mSearchAutoComplete.setForceIgnoreOutsideTouch(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(), this, mSearchable, - mOutsideDrawablesCache); - mSearchAutoComplete.setAdapter(mSuggestionsAdapter); - } - } - - private void updateSearchButton() { - String textLabel = null; - Drawable iconLabel = null; - int textId = mSearchable.getSearchButtonText(); - if (isBrowserSearch()){ - iconLabel = getContext().getResources() - .getDrawable(com.android.internal.R.drawable.ic_btn_search_go); - } else if (textId != 0) { - textLabel = mActivityContext.getResources().getString(textId); - } else { - iconLabel = getContext().getResources(). - getDrawable(com.android.internal.R.drawable.ic_btn_search); - } - mGoButton.setText(textLabel); - mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null); + mSearchAutoComplete.setForceIgnoreOutsideTouch(false); } private void updateSearchAppIcon() { - if (isBrowserSearch()) { - mAppIcon.setImageResource(0); - mAppIcon.setVisibility(View.GONE); - mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL, - mSearchPlate.getPaddingTop(), - mSearchPlate.getPaddingRight(), - mSearchPlate.getPaddingBottom()); - } else { - PackageManager pm = getContext().getPackageManager(); - Drawable icon; - try { - ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0); - icon = pm.getApplicationIcon(info.applicationInfo); - if (DBG) Log.d(LOG_TAG, "Using app-specific icon"); - } catch (NameNotFoundException e) { - icon = pm.getDefaultActivityIcon(); - Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon"); - } - mAppIcon.setImageDrawable(icon); - mAppIcon.setVisibility(View.VISIBLE); - mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, - mSearchPlate.getPaddingTop(), - mSearchPlate.getPaddingRight(), - mSearchPlate.getPaddingBottom()); + PackageManager pm = getContext().getPackageManager(); + Drawable icon; + try { + ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0); + icon = pm.getApplicationIcon(info.applicationInfo); + if (DBG) + Log.d(LOG_TAG, "Using app-specific icon"); + } catch (NameNotFoundException e) { + icon = pm.getDefaultActivityIcon(); + Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon"); } + mAppIcon.setImageDrawable(icon); + mAppIcon.setVisibility(View.VISIBLE); + mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, mSearchPlate.getPaddingTop(), mSearchPlate.getPaddingRight(), mSearchPlate.getPaddingBottom()); } /** @@ -546,7 +438,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS int visibility = View.GONE; Drawable icon = null; CharSequence text = null; - + // optionally show one or the other. if (mSearchable.useBadgeIcon()) { icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId()); @@ -557,71 +449,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS visibility = View.VISIBLE; if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId()); } - + mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); mBadgeLabel.setText(text); mBadgeLabel.setVisibility(visibility); } - /** - * Update the hint in the query text field. - */ - private void updateQueryHint() { - if (isShowing()) { - String hint = null; - if (mSearchable != null) { - int hintId = mSearchable.getHintId(); - if (hintId != 0) { - hint = mActivityContext.getString(hintId); - } - } - mSearchAutoComplete.setHint(hint); - } - } - - /** - * Update the visibility of the voice button. There are actually two voice search modes, - * either of which will activate the button. - * @param empty whether the search query text field is empty. If it is, then the other - * criteria apply to make the voice button visible. Otherwise the voice button will not - * be visible - i.e., if the user has typed a query, remove the voice button. - */ - private void updateVoiceButton(boolean empty) { - int visibility = View.GONE; - if ((mAppSearchData == null || !mAppSearchData.getBoolean( - SearchManager.DISABLE_VOICE_SEARCH, false)) - && mSearchable.getVoiceSearchEnabled() && empty) { - Intent testIntent = null; - if (mSearchable.getVoiceSearchLaunchWebSearch()) { - testIntent = mVoiceWebSearchIntent; - } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { - testIntent = mVoiceAppSearchIntent; - } - if (testIntent != null) { - ResolveInfo ri = getContext().getPackageManager(). - resolveActivity(testIntent, PackageManager.MATCH_DEFAULT_ONLY); - if (ri != null) { - visibility = View.VISIBLE; - } - } - } - mVoiceButton.setVisibility(visibility); - } - - /** Called by SuggestionsAdapter when the cursor contents changed. */ - void onDataSetChanged() { - if (mSearchAutoComplete != null && mSuggestionsAdapter != null) { - mSearchAutoComplete.onFilterComplete(mSuggestionsAdapter.getCount()); - } - } - - /** - * Hack to determine whether this is the browser, so we can adjust the UI. - */ - private boolean isBrowserSearch() { - return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/"); - } - /* * Listeners of various types */ @@ -642,7 +475,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // 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(); @@ -651,336 +484,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS || (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. - * @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) { - if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")"); - if (mSearchable == null) { - return false; - } - - // 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.getQueryActionMsg() != null)) { - launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); - return true; - } - - return super.onKeyDown(keyCode, event); - } - - /** - * 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 onTextChanged(CharSequence s, int start, - int before, int after) { - if (DBG_LOG_TIMING) { - dbgLogTiming("onTextChanged()"); - } - if (mSearchable == null) { - return; - } - if (!mSearchAutoComplete.isPerformingCompletion()) { - // The user changed the query, remember it. - mUserQuery = s == null ? "" : s.toString(); - } - updateWidgetState(); - // Always want to show the microphone if the context is voice. - // Also show the microphone if this is a browser search and the - // query matches the initial query. - updateVoiceButton(mSearchAutoComplete.isEmpty() - || (isBrowserSearch() && mInitialQuery.equals(mUserQuery)) - || (mAppSearchData != null && mAppSearchData.getBoolean( - SearchManager.CONTEXT_IS_VOICE))); - } - - public void afterTextChanged(Editable s) { - if (mSearchable == null) { - return; - } - if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) { - // The user changed the query, check if it is a URL and if so change the search - // button in the soft keyboard to the 'Go' button. - int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)) - | EditorInfo.IME_ACTION_GO; - if (options != mSearchAutoCompleteImeOptions) { - mSearchAutoCompleteImeOptions = options; - mSearchAutoComplete.setImeOptions(options); - // This call is required to update the soft keyboard UI with latest IME flags. - mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType()); - } - } - } - }; - - /** - * Enable/Disable the go button based on edit text state (any text?) - */ - private void updateWidgetState() { - // enable the button if we have one or more non-space characters - boolean enabled = !mSearchAutoComplete.isEmpty(); - if (isBrowserSearch()) { - // In the browser, we hide the search button when there is no text, - // or if the text matches the initial query. - if (enabled && !mInitialQuery.equals(mUserQuery)) { - mSearchAutoComplete.setBackgroundResource( - com.android.internal.R.drawable.textfield_search); - mGoButton.setVisibility(View.VISIBLE); - // Just to be sure - mGoButton.setEnabled(true); - mGoButton.setFocusable(true); - } else { - mSearchAutoComplete.setBackgroundResource( - com.android.internal.R.drawable.textfield_search_empty); - mGoButton.setVisibility(View.GONE); - } - } else { - // Elsewhere we just disable the button - mGoButton.setEnabled(enabled); - mGoButton.setFocusable(enabled); - } - } - - /** - * 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) { - // guard against possible race conditions - if (mSearchable == null) { - return false; - } - - if (!event.isSystem() && - (keyCode != KeyEvent.KEYCODE_DPAD_UP) && - (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; - } - }; - - /** - * React to a click in the GO button by launching a search. - */ - View.OnClickListener mGoButtonClickListener = new View.OnClickListener() { - public void onClick(View v) { - // guard against possible race conditions - if (mSearchable == null) { - return; - } - launchQuerySearch(); - } - }; - - /** - * React to a click in the voice search button. - */ - View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() { - public void onClick(View v) { - // guard against possible race conditions - if (mSearchable == null) { - return; - } - SearchableInfo searchable = mSearchable; - try { - if (searchable.getVoiceSearchLaunchWebSearch()) { - Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent, - searchable); - getContext().startActivity(webSearchIntent); - } else if (searchable.getVoiceSearchLaunchRecognizer()) { - Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, - searchable); - getContext().startActivity(appSearchIntent); - } - } catch (ActivityNotFoundException e) { - // Should not happen, since we check the availability of - // voice search before showing the button. But just in case... - Log.w(LOG_TAG, "Could not find voice search activity"); - } - dismiss(); - } - }; - - /** - * Create and return an Intent that can launch the voice search activity for web search. - */ - private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) { - Intent voiceIntent = new Intent(baseIntent); - ComponentName searchActivity = searchable.getSearchActivity(); - voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, - searchActivity == null ? null : searchActivity.flattenToShortString()); - return voiceIntent; - } - - /** - * Create and return an Intent that can launch the voice search activity, perform a specific - * voice transcription, and forward the results to the searchable activity. - * - * @param baseIntent The voice app search intent to start from - * @return A completely-configured intent ready to send to the voice search activity - */ - private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { - ComponentName searchActivity = searchable.getSearchActivity(); - - // create the necessary intent to set up a search-and-forward operation - // in the voice search system. We have to keep the bundle separate, - // because it becomes immutable once it enters the PendingIntent - Intent queryIntent = new Intent(Intent.ACTION_SEARCH); - queryIntent.setComponent(searchActivity); - PendingIntent pending = PendingIntent.getActivity( - getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT); - - // Now set up the bundle that will be inserted into the pending intent - // when it's time to do the search. We always build it here (even if empty) - // because the voice search activity will always need to insert "QUERY" into - // it anyway. - Bundle queryExtras = new Bundle(); - if (mAppSearchData != null) { - queryExtras.putBundle(SearchManager.APP_DATA, mAppSearchData); - } - - // Now build the intent to launch the voice search. Add all necessary - // extras to launch the voice recognizer, and then all the necessary extras - // to forward the results to the searchable activity - Intent voiceIntent = new Intent(baseIntent); - - // Add all of the configuration options supplied by the searchable's metadata - String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM; - String prompt = null; - String language = null; - int maxResults = 1; - Resources resources = mActivityContext.getResources(); - if (searchable.getVoiceLanguageModeId() != 0) { - languageModel = resources.getString(searchable.getVoiceLanguageModeId()); - } - if (searchable.getVoicePromptTextId() != 0) { - prompt = resources.getString(searchable.getVoicePromptTextId()); - } - if (searchable.getVoiceLanguageId() != 0) { - language = resources.getString(searchable.getVoiceLanguageId()); - } - if (searchable.getVoiceMaxResults() != 0) { - maxResults = searchable.getVoiceMaxResults(); - } - voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); - voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); - voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); - voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); - voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, - searchActivity == null ? null : searchActivity.flattenToShortString()); - - // Add the values that configure forwarding the results - voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending); - voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras); - - return voiceIntent; - } - - /** - * Corrects http/https typo errors in the given url string, and if the protocol specifier was - * not present defaults to http. - * - * @param inUrl URL to check and fix - * @return fixed URL string. - */ - private String fixUrl(String inUrl) { - if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) - return inUrl; - - if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) { - if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { - inUrl = inUrl.replaceFirst("/", "//"); - } else { - inUrl = inUrl.replaceFirst(":", "://"); - } - } - - if (inUrl.indexOf("://") == -1) { - inUrl = "http://" + inUrl; - } - - return inUrl; - } - - /** - * React to the user typing "enter" or other hardwired keys while typing in the search box. - * This handles these special keys while the edit box has focus. - */ - View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { - public boolean onKey(View v, int keyCode, KeyEvent event) { - // guard against possible race conditions - if (mSearchable == null) { - return false; - } - - if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()"); - if (DBG) { - Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event - + "), selection: " + mSearchAutoComplete.getListSelection()); - } - - // 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); - } - - // 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(); - - // If this is a url entered by the user & we displayed the 'Go' button which - // the user clicked, launch the url instead of using it as a search query. - if (mSearchable.autoUrlDetect() && - (mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION) - == EditorInfo.IME_ACTION_GO) { - Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString())); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - launchIntent(intent); - } else { - // Launch as a regular search. - launchQuerySearch(); - } - return true; - } - if (event.getAction() == KeyEvent.ACTION_DOWN) { - SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); - if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { - launchQuerySearch(keyCode, actionKey.getQueryActionMsg()); - return true; - } - } - } - return false; - } - }; @Override public void hide() { @@ -997,78 +500,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS super.hide(); } - - /** - * 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. - */ - 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) { - 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 = 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, 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 : mSearchAutoComplete.length(); - mSearchAutoComplete.setSelection(selPoint); - mSearchAutoComplete.setListSelection(0); - mSearchAutoComplete.clearListSelection(); - mSearchAutoComplete.ensureImeVisible(true); - - return true; - } - - // Next, check for an "up and out" move - if (keyCode == KeyEvent.KEYCODE_DPAD_UP - && 0 == mSearchAutoComplete.getListSelection()) { - restoreUserQuery(); - // let ACTV complete the move - return false; - } - - // Next, check for an "action key" - SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); - if ((actionKey != null) && - ((actionKey.getSuggestActionMsg() != null) || - (actionKey.getSuggestActionMsgColumn() != null))) { - // 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)) { - return launchSuggestion(position, keyCode, actionMsg); - } - } - } - } - } - return false; - } /** * Launch a search for the text in the query text field. */ - public void launchQuerySearch() { + public void launchQuerySearch() { launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null); } @@ -1080,50 +516,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * @param actionMsg The message for the action key that was pressed, * or <code>null</code> if none. */ - protected void launchQuerySearch(int actionKey, String actionMsg) { + protected void launchQuerySearch(int actionKey, String actionMsg) { String query = mSearchAutoComplete.getText().toString(); String action = Intent.ACTION_SEARCH; - Intent intent = createIntent(action, null, null, query, - actionKey, actionMsg); + Intent intent = createIntent(action, null, null, query, actionKey, actionMsg); launchIntent(intent); } - - /** - * Launches an intent based on a suggestion. - * - * @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). - */ - 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) { - if (mSuggestionsAdapter == null) { - return false; - } - Cursor c = mSuggestionsAdapter.getCursor(); - if ((c != null) && c.moveToPosition(position)) { - - Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); - - // launch the intent - launchIntent(intent); - - return true; - } - return false; - } /** * Launches an intent, including any special intent handling. @@ -1149,21 +547,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * If the intent is to open an HTTP or HTTPS URL, we set - * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that - * has been opened by us for the same URL will be reused. - */ - private void setBrowserApplicationId(Intent intent) { - Uri data = intent.getData(); - if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) { - String scheme = data.getScheme(); - if (scheme != null && scheme.startsWith("http")) { - intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString()); - } - } - } - - /** * Sets the list item selection in the AutoCompleteTextView's ListView. */ public void setListSelection(int index) { @@ -1171,62 +554,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * When a particular suggestion has been selected, perform the various lookups required - * to use the suggestion. This includes checking the cursor for suggestion-specific data, - * and/or falling back to the XML for defaults; It also creates REST style Uri data when - * the suggestion includes a data id. - * - * @param c The suggestions cursor, moved to the row of the user's selection - * @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. - */ - private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) { - try { - // use specific action if supplied, or default action if supplied, or fixed default - String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); - - if (action == null) { - action = mSearchable.getSuggestIntentAction(); - } - if (action == null) { - action = Intent.ACTION_SEARCH; - } - - // use specific data if supplied, or default data if supplied - String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA); - if (data == null) { - data = mSearchable.getSuggestIntentData(); - } - // then, if an ID was provided, append it. - if (data != null) { - String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); - if (id != null) { - data = data + "/" + Uri.encode(id); - } - } - Uri dataUri = (data == null) ? null : Uri.parse(data); - - String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); - String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); - - return createIntent(action, dataUri, extraData, query, actionKey, actionMsg); - } catch (RuntimeException e ) { - int rowNum; - try { // be really paranoid now - rowNum = c.getPosition(); - } catch (RuntimeException e2 ) { - rowNum = -1; - } - 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. @@ -1271,30 +598,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * 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). - * - * @param c The cursor providing suggestions - * @param actionKey The actionkey record being examined - * - * @return Returns a string, or null if no action key message for this suggestion - */ - 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.getSuggestActionMsgColumn(); - if (column != 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) - if (result == null) { - result = actionKey.getSuggestActionMsg(); - } - return result; - } - - /** * The root element in the search bar layout. This is a custom view just to override * the handling of the back button. */ @@ -1315,21 +618,22 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * Overrides the handling of the back key to move back to the previous sources or dismiss - * the search dialog, instead of dismissing the input method. + * Overrides the handling of the back key to move back to the previous + * sources or dismiss the search dialog, instead of dismissing the input + * method. */ @Override public boolean dispatchKeyEventPreIme(KeyEvent event) { - if (DBG) Log.d(LOG_TAG, "onKeyPreIme(" + event + ")"); + if (DBG) + Log.d(LOG_TAG, "onKeyPreIme(" + event + ")"); if (mSearchDialog != null && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null) { - if (event.getAction() == KeyEvent.ACTION_DOWN - && event.getRepeatCount() == 0) { + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { state.startTracking(event, this); return true; - } else if (event.getAction() == KeyEvent.ACTION_UP - && !event.isCanceled() && state.isTracking(event)) { + } else if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled() + && state.isTracking(event)) { mSearchDialog.onBackPressed(); return true; } @@ -1339,86 +643,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } } - /** - * Local subclass for AutoCompleteTextView. - */ - public static class SearchAutoComplete extends AutoCompleteTextView { - - private int mThreshold; - - public SearchAutoComplete(Context context) { - super(context); - 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(); - } - - @Override - public void setThreshold(int threshold) { - super.setThreshold(threshold); - mThreshold = threshold; - } - - /** - * Returns true if the text field is empty, or contains only whitespace. - */ - private boolean isEmpty() { - return TextUtils.getTrimmedLength(getText()) == 0; - } - - /** - * We override this method to avoid replacing the query box text - * when a suggestion is clicked. - */ - @Override - protected void replaceText(CharSequence text) { - } - - /** - * We override this method to avoid an extra onItemClick being called on the - * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} - * when an item is clicked with the trackball. - */ - @Override - public void performCompletion() { - } - - /** - * We override this method to be sure and show the soft keyboard if appropriate when - * the TextView has focus. - */ - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - super.onWindowFocusChanged(hasWindowFocus); - - if (hasWindowFocus) { - InputMethodManager inputManager = (InputMethodManager) - getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - inputManager.showSoftInput(this, 0); - // If in landscape mode, then make sure that - // the ime is in front of the dropdown. - if (isLandscapeMode(getContext())) { - ensureImeVisible(true); - } - } - } - - /** - * We override this method so that we can allow a threshold of zero, which ACTV does not. - */ - @Override - public boolean enoughToFilter() { - return mThreshold <= 0 || super.enoughToFilter(); - } - + private boolean isEmpty(AutoCompleteTextView actv) { + return TextUtils.getTrimmedLength(actv.getText()) == 0; } @Override @@ -1435,106 +661,58 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS cancel(); } - /** - * Implements OnItemClickListener - */ - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); - launchSuggestion(position); + private boolean onClosePressed() { + // Dismiss the dialog if close button is pressed when there's no query text + if (isEmpty(mSearchAutoComplete)) { + dismiss(); + return true; + } + + return false; } - /** - * Implements OnItemSelectedListener - */ - public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - 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) { - 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()); - } + private final SearchView.OnCloseListener mOnCloseListener = new SearchView.OnCloseListener() { - /** - * Debugging Support - */ + public boolean onClose() { + return onClosePressed(); + } + }; + + private final SearchView.OnQueryChangeListener mOnQueryChangeListener = + new SearchView.OnQueryChangeListener() { + + public boolean onSubmitQuery(String query) { + dismiss(); + return false; + } + + public boolean onQueryTextChanged(String newText) { + return false; + } + }; + + private final SearchView.OnSuggestionSelectionListener mOnSuggestionSelectionListener = + new SearchView.OnSuggestionSelectionListener() { + + public boolean onSuggestionSelected(int position) { + return false; + } + + public boolean onSuggestionClicked(int position) { + dismiss(); + return false; + } + }; /** - * For debugging only, sample the millisecond clock and log it. - * Uses AtomicLong so we can use in multiple threads + * Sets the text in the query box, updating the suggestions. */ - private AtomicLong mLastLogTime = new AtomicLong(SystemClock.uptimeMillis()); - private void dbgLogTiming(final String caller) { - long millis = SystemClock.uptimeMillis(); - long oldTime = mLastLogTime.getAndSet(millis); - long delta = millis - oldTime; - final String report = millis + " (+" + delta + ") ticks for Search keystroke in " + caller; - Log.d(LOG_TAG,report); + 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/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java deleted file mode 100644 index 5705bff..0000000 --- a/core/java/android/app/SuggestionsAdapter.java +++ /dev/null @@ -1,653 +0,0 @@ -/* - * 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 com.android.internal.R; - -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.ContentResolver.OpenResourceIdResult; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.net.Uri; -import android.os.Bundle; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.util.Log; -import android.util.SparseArray; -import android.util.TypedValue; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Filter; -import android.widget.ImageView; -import android.widget.ResourceCursorAdapter; -import android.widget.TextView; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -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 static final int QUERY_LIMIT = 50; - - private SearchManager mSearchManager; - private SearchDialog mSearchDialog; - private SearchableInfo mSearchable; - private Context mProviderContext; - private WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache; - private boolean mClosed = false; - - // URL color - private ColorStateList mUrlColor; - - // Cached column indexes, updated when the cursor changes. - private int mText1Col; - private int mText2Col; - private int mText2UrlCol; - private int mIconName1Col; - private int mIconName2Col; - - static final int NONE = -1; - - private final Runnable mStartSpinnerRunnable; - private final Runnable mStopSpinnerRunnable; - - /** - * The amount of time we delay in the filter when the user presses the delete key. - * @see Filter#setDelayer(android.widget.Filter.Delayer). - */ - private static final long DELETE_KEY_POST_DELAY = 500L; - - public SuggestionsAdapter(Context context, SearchDialog searchDialog, - SearchableInfo searchable, - WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) { - super(context, - com.android.internal.R.layout.search_dropdown_item_icons_2line, - null, // no initial cursor - true); // auto-requery - mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); - mSearchDialog = searchDialog; - mSearchable = searchable; - - // set up provider resources (gives us icons, etc.) - Context activityContext = mSearchable.getActivityContext(mContext); - mProviderContext = mSearchable.getProviderContext(mContext, activityContext); - - mOutsideDrawablesCache = outsideDrawablesCache; - - mStartSpinnerRunnable = new Runnable() { - public void run() { - mSearchDialog.setWorking(true); - } - }; - - mStopSpinnerRunnable = new Runnable() { - public void run() { - mSearchDialog.setWorking(false); - } - }; - - // delay 500ms when deleting - getFilter().setDelayer(new Filter.Delayer() { - - private int mPreviousLength = 0; - - public long getPostingDelay(CharSequence constraint) { - if (constraint == null) return 0; - - long delay = constraint.length() < mPreviousLength ? DELETE_KEY_POST_DELAY : 0; - mPreviousLength = constraint.length(); - return delay; - } - }); - } - - /** - * 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(); - /** - * for in app search we show the progress spinner until the cursor is returned with - * the results. - */ - Cursor cursor = null; - mSearchDialog.getWindow().getDecorView().post(mStartSpinnerRunnable); - try { - cursor = mSearchManager.getSuggestions(mSearchable, query, QUERY_LIMIT); - // trigger fill window so the spinner stays up until the results are copied over and - // closer to being ready - if (cursor != null) { - cursor.getCount(); - return cursor; - } - } catch (RuntimeException e) { - Log.w(LOG_TAG, "Search suggestions query threw an exception.", e); - } - // If cursor is null or an exception was thrown, stop the spinner and return null. - // changeCursor doesn't get called if cursor is null - mSearchDialog.getWindow().getDecorView().post(mStopSpinnerRunnable); - return null; - } - - public void close() { - if (DBG) Log.d(LOG_TAG, "close()"); - changeCursor(null); - mClosed = true; - } - - @Override - public void notifyDataSetChanged() { - if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged"); - super.notifyDataSetChanged(); - - mSearchDialog.onDataSetChanged(); - - updateSpinnerState(getCursor()); - } - - @Override - public void notifyDataSetInvalidated() { - if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated"); - super.notifyDataSetInvalidated(); - - updateSpinnerState(getCursor()); - } - - private void updateSpinnerState(Cursor cursor) { - Bundle extras = cursor != null ? cursor.getExtras() : null; - if (DBG) { - Log.d(LOG_TAG, "updateSpinnerState - extra = " - + (extras != null - ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS) - : null)); - } - // Check if the Cursor indicates that the query is not complete and show the spinner - if (extras != null - && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) { - mSearchDialog.getWindow().getDecorView().post(mStartSpinnerRunnable); - return; - } - // If cursor is null or is done, stop the spinner - mSearchDialog.getWindow().getDecorView().post(mStopSpinnerRunnable); - } - - /** - * Cache columns. - */ - @Override - public void changeCursor(Cursor c) { - if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")"); - - if (mClosed) { - Log.w(LOG_TAG, "Tried to change cursor after adapter was closed."); - if (c != null) c.close(); - return; - } - - try { - super.changeCursor(c); - - if (c != null) { - mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); - mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); - mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL); - mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); - mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); - } - } catch (Exception e) { - Log.e(LOG_TAG, "error changing cursor and caching columns", e); - } - } - - /** - * 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(); - - if (views.mText1 != null) { - String text1 = getStringOrNull(cursor, mText1Col); - setViewText(views.mText1, text1); - } - if (views.mText2 != null) { - // First check TEXT_2_URL - CharSequence text2 = getStringOrNull(cursor, mText2UrlCol); - if (text2 != null) { - text2 = formatUrl(text2); - } else { - text2 = getStringOrNull(cursor, mText2Col); - } - - // If no second line of text is indicated, allow the first line of text - // to be up to two lines if it wants to be. - if (TextUtils.isEmpty(text2)) { - if (views.mText1 != null) { - views.mText1.setSingleLine(false); - views.mText1.setMaxLines(2); - } - } else { - if (views.mText1 != null) { - views.mText1.setSingleLine(true); - views.mText1.setMaxLines(1); - } - } - setViewText(views.mText2, text2); - } - - if (views.mIcon1 != null) { - setViewDrawable(views.mIcon1, getIcon1(cursor)); - } - if (views.mIcon2 != null) { - setViewDrawable(views.mIcon2, getIcon2(cursor)); - } - } - - private CharSequence formatUrl(CharSequence url) { - if (mUrlColor == null) { - // Lazily get the URL color from the current theme. - TypedValue colorValue = new TypedValue(); - mContext.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true); - mUrlColor = mContext.getResources().getColorStateList(colorValue.resourceId); - } - - SpannableString text = new SpannableString(url); - text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null), - 0, url.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - return text; - } - - private void setViewText(TextView v, CharSequence text) { - // 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 Drawable getIcon1(Cursor cursor) { - if (mIconName1Col < 0) { - return null; - } - String value = cursor.getString(mIconName1Col); - Drawable drawable = getDrawableFromResourceValue(value); - if (drawable != null) { - return drawable; - } - return getDefaultIcon1(cursor); - } - - private Drawable getIcon2(Cursor cursor) { - if (mIconName2Col < 0) { - return null; - } - String value = cursor.getString(mIconName2Col); - return getDrawableFromResourceValue(value); - } - - /** - * Sets the drawable in an image view, makes sure the view is only visible if there - * is a drawable. - */ - private void setViewDrawable(ImageView v, Drawable drawable) { - // 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); - - // This is a hack to get any animated drawables (like a 'working' spinner) - // to animate. You have to setVisible true on an AnimationDrawable to get - // it to start animating, but it must first have been false or else the - // call to setVisible will be ineffective. We need to clear up the story - // about animated drawables in the future, see http://b/1878430. - drawable.setVisible(false, false); - drawable.setVisible(true, false); - } - } - - /** - * 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.shouldRewriteQueryFromData()) { - String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA); - if (data != null) { - return data; - } - } - - if (mSearchable.shouldRewriteQueryFromText()) { - 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; - } - try { - // First, see if it's just an integer - int resourceId = Integer.parseInt(drawableId); - // It's an int, look for it in the cache - String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE - + "://" + mProviderContext.getPackageName() + "/" + resourceId; - // Must use URI as cache key, since ints are app-specific - Drawable drawable = checkIconCache(drawableUri); - if (drawable != null) { - return drawable; - } - // Not cached, find it by resource ID - drawable = mProviderContext.getResources().getDrawable(resourceId); - // Stick it in the cache, using the URI as key - storeInIconCache(drawableUri, drawable); - return drawable; - } catch (NumberFormatException nfe) { - // It's not an integer, use it as a URI - Drawable drawable = checkIconCache(drawableId); - if (drawable != null) { - return drawable; - } - Uri uri = Uri.parse(drawableId); - drawable = getDrawable(uri); - storeInIconCache(drawableId, drawable); - return drawable; - } catch (Resources.NotFoundException nfe) { - // It was an integer, but it couldn't be found, bail out - Log.w(LOG_TAG, "Icon resource not found: " + drawableId); - return null; - } - } - - /** - * Gets a drawable by URI, without using the cache. - * - * @return A drawable, or {@code null} if the drawable could not be loaded. - */ - private Drawable getDrawable(Uri uri) { - try { - String scheme = uri.getScheme(); - if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { - // Load drawables through Resources, to get the source density information - OpenResourceIdResult r = - mProviderContext.getContentResolver().getResourceId(uri); - try { - return r.r.getDrawable(r.id); - } catch (Resources.NotFoundException ex) { - throw new FileNotFoundException("Resource does not exist: " + uri); - } - } else { - // Let the ContentResolver handle content and file URIs. - InputStream stream = mProviderContext.getContentResolver().openInputStream(uri); - if (stream == null) { - throw new FileNotFoundException("Failed to open " + uri); - } - try { - return Drawable.createFromStream(stream, null); - } finally { - try { - stream.close(); - } catch (IOException ex) { - Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex); - } - } - } - } catch (FileNotFoundException fnfe) { - Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage()); - return null; - } - } - - private Drawable checkIconCache(String resourceUri) { - Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri); - if (cached == null) { - return null; - } - if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri); - return cached.newDrawable(); - } - - private void storeInIconCache(String resourceUri, Drawable drawable) { - if (drawable != null) { - mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState()); - } - } - - /** - * Gets the left-hand side icon that will be used for the current suggestion - * if the suggestion contains an icon column but no icon or a broken icon. - * - * @param cursor A cursor positioned at the current suggestion. - * @return A non-null drawable. - */ - private Drawable getDefaultIcon1(Cursor cursor) { - // Check the component that gave us the suggestion - Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity()); - if (drawable != null) { - return drawable; - } - - // Fall back to a default icon - return mContext.getPackageManager().getDefaultActivityIcon(); - } - - /** - * Gets the activity or application icon for an activity. - * Uses the local icon cache for fast repeated lookups. - * - * @param component Name of an activity. - * @return A drawable, or {@code null} if neither the activity nor the application - * has an icon set. - */ - private Drawable getActivityIconWithCache(ComponentName component) { - // First check the icon cache - String componentIconKey = component.flattenToShortString(); - // Using containsKey() since we also store null values. - if (mOutsideDrawablesCache.containsKey(componentIconKey)) { - Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey); - return cached == null ? null : cached.newDrawable(mProviderContext.getResources()); - } - // Then try the activity or application icon - Drawable drawable = getActivityIcon(component); - // Stick it in the cache so we don't do this lookup again. - Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState(); - mOutsideDrawablesCache.put(componentIconKey, toCache); - return drawable; - } - - /** - * Gets the activity or application icon for an activity. - * - * @param component Name of an activity. - * @return A drawable, or {@code null} if neither the acitivy or the application - * have an icon set. - */ - private Drawable getActivityIcon(ComponentName component) { - PackageManager pm = mContext.getPackageManager(); - final ActivityInfo activityInfo; - try { - activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA); - } catch (NameNotFoundException ex) { - Log.w(LOG_TAG, ex.toString()); - return null; - } - int iconId = activityInfo.getIconResource(); - if (iconId == 0) return null; - String pkg = component.getPackageName(); - Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo); - if (drawable == null) { - Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for " - + component.flattenToShortString()); - return 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); - return getStringOrNull(cursor, col); - } - - private static String getStringOrNull(Cursor cursor, int col) { - if (col == NONE) { - return null; - } - try { - return cursor.getString(col); - } catch (Exception e) { - Log.e(LOG_TAG, - "unexpected error retrieving valid column from cursor, " - + "did the remote process die?", e); - return null; - } - } -} diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index 029aebf..b296b77 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -18,8 +18,6 @@ package android.widget; import static android.widget.SuggestionsAdapter.getColumnString; -import com.android.internal.R; - import android.app.PendingIntent; import android.app.SearchManager; import android.app.SearchableInfo; @@ -29,6 +27,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.Cursor; @@ -50,6 +49,8 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.TextView.OnEditorActionListener; +import com.android.internal.R; + import java.util.WeakHashMap; /** @@ -84,7 +85,7 @@ public class SearchView extends LinearLayout { private View mCloseButton; private View mSearchEditFrame; private View mVoiceButton; - private AutoCompleteTextView mQueryTextView; + private SearchAutoComplete mQueryTextView; private boolean mSubmitButtonEnabled; private CharSequence mQueryHint; private boolean mQueryRefinement; @@ -181,7 +182,9 @@ public class SearchView extends LinearLayout { inflater.inflate(R.layout.search_view, this, true); mSearchButton = findViewById(R.id.search_button); - mQueryTextView = (AutoCompleteTextView) findViewById(R.id.search_src_text); + mQueryTextView = (SearchAutoComplete) findViewById(R.id.search_src_text); + mQueryTextView.setSearchView(this); + mSearchEditFrame = findViewById(R.id.search_edit_frame); mSubmitButton = findViewById(R.id.search_go_btn); mCloseButton = findViewById(R.id.search_close_btn); @@ -196,6 +199,7 @@ public class SearchView extends LinearLayout { mQueryTextView.setOnEditorActionListener(mOnEditorActionListener); mQueryTextView.setOnItemClickListener(mOnItemClickListener); mQueryTextView.setOnItemSelectedListener(mOnItemSelectedListener); + mQueryTextView.setOnKeyListener(mTextKeyListener); // Inform any listener of focus changes mQueryTextView.setOnFocusChangeListener(new OnFocusChangeListener() { @@ -558,6 +562,148 @@ public class SearchView extends LinearLayout { return super.onKeyDown(keyCode, event); } + /** + * React to the user typing "enter" or other hardwired keys while typing in + * the search box. This handles these special keys while the edit box has + * focus. + */ + View.OnKeyListener mTextKeyListener = new View.OnKeyListener() { + public boolean onKey(View v, int keyCode, KeyEvent event) { + // guard against possible race conditions + if (mSearchable == null) { + return false; + } + + if (DBG) { + Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: " + + mQueryTextView.getListSelection()); + } + + // If a suggestion is selected, handle enter, search key, and action keys + // as presses on the selected suggestion + if (mQueryTextView.isPopupShowing() + && mQueryTextView.getListSelection() != ListView.INVALID_POSITION) { + return onSuggestionsKey(v, keyCode, event); + } + + // If there is text in the query box, handle enter, and action keys + // The search key is handled by the dialog's onKeyDown(). + if (!mQueryTextView.isEmpty()) { + if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { + v.cancelLongPress(); + + // Launch as a regular search. + launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mQueryTextView.getText() + .toString()); + return true; + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); + if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) { + launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mQueryTextView + .getText().toString()); + return true; + } + } + } + return false; + } + }; + + /** + * 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. + */ + 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) { + + // 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 = mQueryTextView.getListSelection(); + return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); + } + + // 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, 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 : mQueryTextView + .length(); + mQueryTextView.setSelection(selPoint); + mQueryTextView.setListSelection(0); + mQueryTextView.clearListSelection(); + mQueryTextView.ensureImeVisible(true); + + return true; + } + + // Next, check for an "up and out" move + if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mQueryTextView.getListSelection()) { + // TODO: restoreUserQuery(); + // let ACTV complete the move + return false; + } + + // Next, check for an "action key" + SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); + if ((actionKey != null) + && ((actionKey.getSuggestActionMsg() != null) || (actionKey + .getSuggestActionMsgColumn() != null))) { + // launch suggestion using action key column + int position = mQueryTextView.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)) { + return onItemClicked(position, keyCode, actionMsg); + } + } + } + } + } + return false; + } + + /** + * 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). + * + * @param c The cursor providing suggestions + * @param actionKey The actionkey record being examined + * + * @return Returns a string, or null if no action key message for this + * suggestion + */ + 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.getSuggestActionMsgColumn(); + if (column != 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) + if (result == null) { + result = actionKey.getSuggestActionMsg(); + } + return result; + } + private void updateQueryHint() { if (mQueryHint != null) { mQueryTextView.setHint(mQueryHint); @@ -710,20 +856,34 @@ public class SearchView extends LinearLayout { } } + private boolean onItemClicked(int position, int actionKey, String actionMsg) { + if (mOnSuggestionListener == null + || !mOnSuggestionListener.onSuggestionClicked(position)) { + launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); + setImeVisibility(false); + dismissSuggestions(); + return true; + } + return false; + } + + private boolean onItemSelected(int position) { + if (mOnSuggestionListener == null + || !mOnSuggestionListener.onSuggestionSelected(position)) { + rewriteQueryFromSuggestion(position); + return true; + } + return false; + } + private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() { /** * Implements OnItemClickListener */ public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - if (DBG) - Log.d(LOG_TAG, "onItemClick() position " + position); - if (mOnSuggestionListener == null - || !mOnSuggestionListener.onSuggestionClicked(position)) { - launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); - setImeVisibility(false); - dismissSuggestions(); - } + if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position); + onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null); } }; @@ -733,14 +893,8 @@ public class SearchView extends LinearLayout { * Implements OnItemSelectedListener */ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - 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 (mOnSuggestionListener == null - || !mOnSuggestionListener.onSuggestionSelected(position)) { - rewriteQueryFromSuggestion(position); - } + if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position); + SearchView.this.onItemSelected(position); } /** @@ -1003,6 +1157,11 @@ public class SearchView extends LinearLayout { } } + static boolean isLandscapeMode(Context context) { + return context.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } + /** * Callback to watch the text field for empty/non-empty */ @@ -1018,4 +1177,93 @@ public class SearchView extends LinearLayout { public void afterTextChanged(Editable s) { } }; + + /** + * Local subclass for AutoCompleteTextView. + * @hide + */ + public static class SearchAutoComplete extends AutoCompleteTextView { + + private int mThreshold; + private SearchView mSearchView; + + public SearchAutoComplete(Context context) { + super(context); + 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(); + } + + void setSearchView(SearchView searchView) { + mSearchView = searchView; + } + + @Override + public void setThreshold(int threshold) { + super.setThreshold(threshold); + mThreshold = threshold; + } + + /** + * Returns true if the text field is empty, or contains only whitespace. + */ + private boolean isEmpty() { + return TextUtils.getTrimmedLength(getText()) == 0; + } + + /** + * We override this method to avoid replacing the query box text when a + * suggestion is clicked. + */ + @Override + protected void replaceText(CharSequence text) { + } + + /** + * We override this method to avoid an extra onItemClick being called on + * the drop-down's OnItemClickListener by + * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is + * clicked with the trackball. + */ + @Override + public void performCompletion() { + } + + /** + * We override this method to be sure and show the soft keyboard if + * appropriate when the TextView has focus. + */ + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + if (hasWindowFocus) { + InputMethodManager inputManager = (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.showSoftInput(this, 0); + // If in landscape mode, then make sure that + // the ime is in front of the dropdown. + if (isLandscapeMode(getContext())) { + ensureImeVisible(true); + } + } + } + + /** + * We override this method so that we can allow a threshold of zero, + * which ACTV does not. + */ + @Override + public boolean enoughToFilter() { + return mThreshold <= 0 || super.enoughToFilter(); + } + } } diff --git a/core/res/res/layout/search_bar.xml b/core/res/res/layout/search_bar.xml index 7935e2a..4bc2d1a 100644 --- a/core/res/res/layout/search_bar.xml +++ b/core/res/res/layout/search_bar.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- -/* apps/common/res/layout/SearchBar.xml +/* ** ** Copyright 2007, The Android Open Source Project ** @@ -23,87 +23,36 @@ android:id="@+id/search_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" + android:orientation="horizontal" android:focusable="true" + android:background="?attr/actionModeBackground" android:descendantFocusability="afterDescendants"> - <!-- Outer layout defines the entire search bar at the top of the screen --> - <LinearLayout - android:id="@+id/search_plate" + <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:paddingLeft="12dip" - android:paddingRight="6dip" - android:paddingTop="7dip" - android:paddingBottom="16dip" - android:background="@drawable/search_plate_global" > + android:layout_gravity="center_vertical" + > - <!-- This is actually used for the badge icon *or* the badge label (or neither) --> - <TextView - android:id="@+id/search_badge" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginBottom="2dip" - android:drawablePadding="0dip" - android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:attr/textColorPrimaryInverse" /> - - <!-- Inner layout contains the app icon, button(s) and EditText --> - <LinearLayout - android:id="@+id/search_edit_frame" + <ImageView + android:id="@+id/search_app_icon" + android:layout_height="48dip" + android:layout_width="48dip" + android:layout_marginLeft="8dip" + android:layout_marginRight="8dip" + android:layout_gravity="center_vertical" + android:layout_alignParentLeft="true" + /> + <SearchView + android:id="@+id/search_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal"> - - <ImageView - android:id="@+id/search_app_icon" - android:layout_height="36dip" - android:layout_width="36dip" - android:layout_marginRight="7dip" - android:layout_gravity="center_vertical" - /> - - <view class="android.app.SearchDialog$SearchAutoComplete" - android:id="@+id/search_src_text" - android:background="@drawable/textfield_search" - android:layout_height="wrap_content" - android:layout_width="0dip" - android:layout_weight="1.0" - android:paddingLeft="8dip" - android:paddingRight="6dip" - android:drawablePadding="2dip" - android:singleLine="true" - android:ellipsize="end" - android:inputType="text|textAutoComplete" - android:dropDownWidth="match_parent" - android:dropDownHeight="match_parent" - android:dropDownAnchor="@id/search_plate" - android:dropDownVerticalOffset="-9dip" - android:popupBackground="@android:drawable/search_dropdown_background" - /> - - <!-- This button can switch between text and icon "modes" --> - <Button - android:id="@+id/search_go_btn" - android:background="@drawable/btn_search_dialog" - android:layout_width="wrap_content" - android:layout_height="match_parent" + android:maxWidth="600dip" + android:iconifiedByDefault="false" + android:layout_alignParentRight="true" + android:layout_gravity="center_vertical|right" /> - <ImageButton - android:id="@+id/search_voice_btn" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_marginLeft="0dip" - android:layout_marginTop="-6.5dip" - android:layout_marginBottom="-7dip" - android:layout_marginRight="-5dip" - android:background="@drawable/btn_search_dialog_voice" - android:src="@android:drawable/ic_btn_speak_now" - /> - </LinearLayout> - - </LinearLayout> + </RelativeLayout> </view> diff --git a/core/res/res/layout/search_view.xml b/core/res/res/layout/search_view.xml index eb0bb11..0fb824f 100644 --- a/core/res/res/layout/search_view.xml +++ b/core/res/res/layout/search_view.xml @@ -74,7 +74,7 @@ android:src="?android:attr/searchViewSearchIcon" /> - <AutoCompleteTextView + <view class="android.widget.SearchView$SearchAutoComplete" android:id="@+id/search_src_text" android:layout_height="36dip" android:layout_width="0dp" diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index 8c59360..380d63b 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -563,6 +563,21 @@ <item name="android:windowNoTitle">true</item> </style> + <!-- Default holo light theme for panel windows. This removes all extraneous + window decorations, so you basically have an empty rectangle in which + to place your content. It makes the window floating, with a transparent + background, and turns off dimming behind the window. --> + <style name="Theme.Holo.Light.Panel"> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:colorBackgroundCacheHint">@null</item> + <item name="android:windowFrame">@null</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowIsFloating">true</item> + <item name="android:backgroundDimEnabled">false</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowNoTitle">true</item> + </style> + <!-- Default theme for input methods, which is used by the {@link android.inputmethodservice.InputMethodService} class. this inherits from Theme.NoTitleBar, but makes the background @@ -577,7 +592,7 @@ </style> <!-- Theme for the search input bar. --> - <style name="Theme.SearchBar" parent="Theme.Panel"> + <style name="Theme.SearchBar" parent="Theme.Holo.Light.Panel"> <item name="windowContentOverlay">@null</item> </style> |