diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | c8f00b61c600927ab404c84686d4472e9b527976 (patch) | |
tree | cb28d653b31d6a23067bb0f9de18fa7dc6e3ac37 /src/com/android/launcher/Search.java | |
download | android_packages_apps_Trebuchet-c8f00b61c600927ab404c84686d4472e9b527976.tar.gz android_packages_apps_Trebuchet-c8f00b61c600927ab404c84686d4472e9b527976.tar.bz2 android_packages_apps_Trebuchet-c8f00b61c600927ab404c84686d4472e9b527976.zip |
Initial Contribution
Diffstat (limited to 'src/com/android/launcher/Search.java')
-rw-r--r-- | src/com/android/launcher/Search.java | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/src/com/android/launcher/Search.java b/src/com/android/launcher/Search.java new file mode 100644 index 000000000..69e26ac59 --- /dev/null +++ b/src/com/android/launcher/Search.java @@ -0,0 +1,594 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher; + +import android.app.ISearchManager; +import android.app.SearchManager; +import android.content.Context; +import android.content.Intent; +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.RemoteException; +import android.os.ServiceManager; +import android.server.search.SearchableInfo; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.view.View.OnLongClickListener; +import android.widget.AdapterView; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CursorAdapter; +import android.widget.Filter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; + +public class Search extends LinearLayout implements OnClickListener, OnKeyListener, + OnLongClickListener, TextWatcher, OnItemClickListener, OnItemSelectedListener { + + private final String TAG = "SearchGadget"; + + private AutoCompleteTextView mSearchText; + private Button mGoButton; + private OnLongClickListener mLongClickListener; + + // Support for suggestions + private SuggestionsAdapter mSuggestionsAdapter; + private SearchableInfo mSearchable; + private String mSuggestionAction = null; + private Uri mSuggestionData = null; + private String mSuggestionQuery = null; + private int mItemSelected = -1; + + /** + * Used to inflate the Workspace from XML. + * + * @param context The application's context. + * @param attrs The attribtues set containing the Workspace's customization values. + */ + public Search(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Implements OnClickListener (for button) + */ + public void onClick(View v) { + query(); + } + + private void query() { + String query = mSearchText.getText().toString(); + if (TextUtils.getTrimmedLength(mSearchText.getText()) == 0) { + return; + } + sendLaunchIntent(Intent.ACTION_SEARCH, null, query, null, 0, null, mSearchable); + } + + /** + * Assemble a search intent and send it. + * + * This is copied from SearchDialog. + * + * @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. + */ + 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); + launcher.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (query != null) { + launcher.putExtra(SearchManager.QUERY, query); + } + + if (data != null) { + launcher.setData(data); + } + + if (appData != null) { + launcher.putExtra(SearchManager.APP_DATA, appData); + } + + // add launch info (action key, etc.) + if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { + launcher.putExtra(SearchManager.ACTION_KEY, actionKey); + launcher.putExtra(SearchManager.ACTION_MSG, actionMsg); + } + + // attempt to enforce security requirement (no 3rd-party intents) + if (si != null) { + launcher.setComponent(si.mSearchActivity); + } + + getContext().startActivity(launcher); + } + + /** + * Implements TextWatcher (for EditText) + */ + public void beforeTextChanged(CharSequence s, int start, int before, int after) { + } + + /** + * Implements TextWatcher (for EditText) + */ + public void onTextChanged(CharSequence s, int start, int before, int after) { + // enable the button if we have one or more non-space characters + boolean enabled = TextUtils.getTrimmedLength(mSearchText.getText()) != 0; + mGoButton.setEnabled(enabled); + mGoButton.setFocusable(enabled); + } + + /** + * Implements TextWatcher (for EditText) + */ + public void afterTextChanged(Editable s) { + } + + /** + * Implements OnKeyListener (for EditText and for button) + * + * This plays some games with state in order to "soften" the strength of suggestions + * presented. Suggestions should not be used unless the user specifically navigates to them + * (or clicks them, in which case it's obvious). This is not the way that AutoCompleteTextBox + * normally works. + */ + public final boolean onKey(View v, int keyCode, KeyEvent event) { + if (v == mSearchText) { + boolean searchTrigger = (keyCode == KeyEvent.KEYCODE_ENTER || + keyCode == KeyEvent.KEYCODE_SEARCH || + keyCode == KeyEvent.KEYCODE_DPAD_CENTER); + if (event.getAction() == KeyEvent.ACTION_UP) { +// Log.d(TAG, "onKey() ACTION_UP isPopupShowing:" + mSearchText.isPopupShowing()); + if (!mSearchText.isPopupShowing()) { + if (searchTrigger) { + query(); + return true; + } + } + } else { +// Log.d(TAG, "onKey() ACTION_DOWN isPopupShowing:" + mSearchText.isPopupShowing() + +// " mItemSelected="+ mItemSelected); + if (searchTrigger && mItemSelected < 0) { + query(); + return true; + } + } + } else if (v == mGoButton) { + 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)) { + if (mSearchText.requestFocus()) { + handled = mSearchText.dispatchKeyEvent(event); + } + } + return handled; + } + + return false; + } + + @Override + public void setOnLongClickListener(OnLongClickListener l) { + super.setOnLongClickListener(l); + mLongClickListener = l; + } + + /** + * Implements OnLongClickListener (for button) + */ + public boolean onLongClick(View v) { + // Pretend that a long press on a child view is a long press on the search widget + if (mLongClickListener != null) { + return mLongClickListener.onLongClick(this); + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + requestFocusFromTouch(); + return super.onInterceptTouchEvent(ev); + } + + /** + * In order to keep things simple, the external trigger will clear the query just before + * focusing, so as to give you a fresh query. This way we eliminate any sources of + * accidental query launching. + */ + public void clearQuery() { + mSearchText.setText(null); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mSearchText = (AutoCompleteTextView) findViewById(R.id.input); + // TODO: This can be confusing when the user taps the text field to give the focus + // (it is not necessary but I ran into this issue several times myself) + // mTitleInput.setOnClickListener(this); + mSearchText.setOnKeyListener(this); + mSearchText.addTextChangedListener(this); + + mGoButton = (Button) findViewById(R.id.go); + mGoButton.setOnClickListener(this); + mGoButton.setOnKeyListener(this); + + mSearchText.setOnLongClickListener(this); + mGoButton.setOnLongClickListener(this); + + // disable the button since we start out w/empty input + mGoButton.setEnabled(false); + mGoButton.setFocusable(false); + + configureSuggestions(); + } + + /** The rest of the class deals with providing search suggestions */ + + /** + * Set up the suggestions provider mechanism + */ + private void configureSuggestions() { + // get SearchableInfo + ISearchManager sms; + SearchableInfo searchable; + sms = ISearchManager.Stub.asInterface(ServiceManager.getService(Context.SEARCH_SERVICE)); + try { + // TODO null isn't the published use of this API, but it works when global=true + // TODO better implementation: defer all of this, let Home set it up + searchable = sms.getSearchableInfo(null, true); + } catch (RemoteException e) { + searchable = null; + } + if (searchable == null) { + // no suggestions so just get out (no need to continue) + return; + } + mSearchable = searchable; + + mSearchText.setOnItemClickListener(this); + mSearchText.setOnItemSelectedListener(this); + + // attach the suggestions adapter + mSuggestionsAdapter = new SuggestionsAdapter(mContext, + com.android.internal.R.layout.search_dropdown_item_1line, null, + SuggestionsAdapter.ONE_LINE_FROM, SuggestionsAdapter.ONE_LINE_TO, mSearchable); + mSearchText.setAdapter(mSuggestionsAdapter); + } + + /** + * Implements OnItemClickListener + */ + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { +// Log.d(TAG, "onItemClick() position " + position); + launchSuggestion(mSuggestionsAdapter, position); + } + + /** + * Implements OnItemSelectedListener + */ + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { +// Log.d(TAG, "onItemSelected() position " + position); + mItemSelected = position; + } + + /** + * Implements OnItemSelectedListener + */ + public void onNothingSelected(AdapterView<?> parent) { +// Log.d(TAG, "onNothingSelected()"); + mItemSelected = -1; + } + + /** + * Code to launch a suggestion query. + * + * This is copied from SearchDialog. + * + * @param ca The CursorAdapter containing the suggestions + * @param position The suggestion we'll be launching from + * + * @return Returns true if a successful launch, false if could not (e.g. bad position) + */ + private boolean launchSuggestion(CursorAdapter ca, int position) { + if (ca != null) { + Cursor c = ca.getCursor(); + if ((c != null) && c.moveToPosition(position)) { + setupSuggestionIntent(c, mSearchable); + + SearchableInfo si = mSearchable; + String suggestionAction = mSuggestionAction; + Uri suggestionData = mSuggestionData; + String suggestionQuery = mSuggestionQuery; + sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, null, + KeyEvent.KEYCODE_UNKNOWN, null, si); + return true; + } + } + return false; + } + + /** + * 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. + * + * NOTE: Return values are in member variables mSuggestionAction, mSuggestionData and + * mSuggestionQuery. + * + * This is copied from SearchDialog. + * + * @param c The suggestions cursor, moved to the row of the user's selection + * @param si The searchable activity's info record + */ + void setupSuggestionIntent(Cursor c, SearchableInfo si) { + try { + // use specific action if supplied, or default action if supplied, or fixed default + mSuggestionAction = null; + int column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION); + if (column >= 0) { + final String action = c.getString(column); + if (action != null) { + mSuggestionAction = action; + } + } + if (mSuggestionAction == null) { + mSuggestionAction = si.getSuggestIntentAction(); + } + if (mSuggestionAction == null) { + mSuggestionAction = Intent.ACTION_SEARCH; + } + + // use specific data if supplied, or default data if supplied + String data = null; + column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA); + if (column >= 0) { + final String rowData = c.getString(column); + if (rowData != null) { + data = rowData; + } + } + if (data == null) { + data = si.getSuggestIntentData(); + } + + // then, if an ID was provided, append it. + if (data != null) { + column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); + if (column >= 0) { + final String id = c.getString(column); + if (id != null) { + data = data + "/" + Uri.encode(id); + } + } + } + mSuggestionData = (data == null) ? null : Uri.parse(data); + + mSuggestionQuery = null; + column = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); + if (column >= 0) { + final String query = c.getString(column); + if (query != null) { + mSuggestionQuery = query; + } + } + } catch (RuntimeException e ) { + int rowNum; + try { // be really paranoid now + rowNum = c.getPosition(); + } catch (RuntimeException e2 ) { + rowNum = -1; + } + Log.w(TAG, "Search Suggestions cursor at row " + rowNum + + " returned exception" + e.toString()); + } + } + + /** + * This class provides the filtering-based interface to suggestions providers. + */ + private static class SuggestionsAdapter extends SimpleCursorAdapter { + public final static String[] ONE_LINE_FROM = { SearchManager.SUGGEST_COLUMN_TEXT_1 }; + public final static int[] ONE_LINE_TO = { com.android.internal.R.id.text1 }; + + private final String TAG = "SuggestionsAdapter"; + + Filter mFilter; + SearchableInfo mSearchable; + private Resources mProviderResources; + String[] mFromStrings; + + public SuggestionsAdapter(Context context, int layout, Cursor c, + String[] from, int[] to, SearchableInfo searchable) { + super(context, layout, c, from, to); + mFromStrings = from; + mSearchable = searchable; + + // set up provider resources (gives us icons, etc.) + Context activityContext = mSearchable.getActivityContext(mContext); + Context providerContext = mSearchable.getProviderContext(mContext, activityContext); + mProviderResources = providerContext.getResources(); + } + + /** + * 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) { + String query = (constraint == null) ? "" : constraint.toString(); + return getSuggestions(mSearchable, query); + } + + /** + * Overriding this allows us to write the selected query back into the box. + * NOTE: This is a vastly simplified version of SearchDialog.jamQuery() and does + * not universally support the search API. But it is sufficient for Google Search. + */ + @Override + public CharSequence convertToString(Cursor cursor) { + CharSequence result = null; + if (cursor != null) { + int column = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY); + if (column >= 0) { + final String query = cursor.getString(column); + if (query != null) { + result = query; + } + } + } + return result; + } + + /** + * Get the query cursor for the search suggestions. + * + * TODO this is functionally identical to the version in SearchDialog.java. Perhaps it + * could be hoisted into SearchableInfo or some other shared spot. + * + * @param query The search text entered (so far) + * @return Returns a cursor with suggestions, or null if no suggestions + */ + private Cursor getSuggestions(final SearchableInfo searchable, final String query) { + Cursor cursor = null; + if (searchable.getSuggestAuthority() != null) { + try { + StringBuilder uriStr = new StringBuilder("content://"); + uriStr.append(searchable.getSuggestAuthority()); + + // if content path provided, insert it now + final String contentPath = searchable.getSuggestPath(); + if (contentPath != null) { + uriStr.append('/'); + uriStr.append(contentPath); + } + + // append standard suggestion query path + uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY); + + // inject query, either as selection args or inline + String[] selArgs = null; + if (searchable.getSuggestSelection() != null) { // use selection if provided + selArgs = new String[] {query}; + } else { + uriStr.append('/'); // no sel, use REST pattern + uriStr.append(Uri.encode(query)); + } + + // finally, make the query + cursor = mContext.getContentResolver().query( + Uri.parse(uriStr.toString()), null, + searchable.getSuggestSelection(), selArgs, + null); + } catch (RuntimeException e) { + Log.w(TAG, "Search Suggestions query returned exception " + e.toString()); + cursor = null; + } + } + + return cursor; + } + + /** + * Overriding this allows us to affect the way that an icon is loaded. Specifically, + * we can be more controlling about the resource path (and allow icons to come from other + * packages). + * + * TODO: This is 100% identical to the version in SearchDialog.java + * + * @param v ImageView to receive an image + * @param value the value retrieved from the cursor + */ + @Override + public void setViewImage(ImageView v, String value) { + int resID; + Drawable img = null; + + try { + resID = Integer.parseInt(value); + if (resID != 0) { + img = mProviderResources.getDrawable(resID); + } + } catch (NumberFormatException nfe) { + // img = null; + } catch (NotFoundException e2) { + // img = null; + } + + // finally, set the image to whatever we've gotten + v.setImageDrawable(img); + } + + /** + * This method is overridden purely to provide a bit of protection against + * flaky content providers. + * + * TODO: This is 100% identical to the version in SearchDialog.java + * + * @see android.widget.ListAdapter#getView(int, View, ViewGroup) + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + try { + return super.getView(position, convertView, parent); + } catch (RuntimeException e) { + Log.w(TAG, "Search Suggestions cursor returned exception " + e.toString()); + // what can I return here? + View v = newView(mContext, mCursor, parent); + if (v != null) { + TextView tv = (TextView) v.findViewById(com.android.internal.R.id.text1); + tv.setText(e.toString()); + } + return v; + } + } + + } +} |