For a guide to creating notifications, see the + * Creating Status + * Bar Notifications document in the Dev Guide.
*/ public class Notification implements Parcelable { @@ -52,7 +55,8 @@ public class Notification implements Parcelable /** * Use the default notification vibrate. This will ignore any given - * {@link #vibrate}. + * {@link #vibrate}. Using phone vibration requires the + * {@link android.Manifest.permission#VIBRATE VIBRATE} permission. * * @see #defaults */ @@ -149,8 +153,7 @@ public class Notification implements Parcelable /** - * The pattern with which to vibrate. This pattern will repeat if {@link - * #FLAG_INSISTENT} bit is set in the {@link #flags} field. + * The pattern with which to vibrate. * ** To vibrate the default pattern, see {@link #defaults}. @@ -228,13 +231,8 @@ public class Notification implements Parcelable /** * Bit to be bitwise-ored into the {@link #flags} field that if set, - * the audio and vibration will be repeated until the notification is - * cancelled. - * - *
- * NOTE: This notion will change when we have decided exactly - * what the UI will be. - *
+ * the audio will be repeated until the notification is + * cancelled or the notification window is opened. */ public static final int FLAG_INSISTENT = 0x00000004; diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index 1bed706763a1a0c11a1ca803fd1c754643a8a1af..cb660c7fde6f55540ceec1f2a1424d0f2001ec87 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -440,9 +440,13 @@ public final class PendingIntent implements Parcelable { @Override public String toString() { - return "PendingIntent{" - + Integer.toHexString(System.identityHashCode(this)) - + " target " + (mTarget != null ? mTarget.asBinder() : null) + "}"; + StringBuilder sb = new StringBuilder(128); + sb.append("PendingIntent{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(": "); + sb.append(mTarget != null ? mTarget.asBinder() : null); + sb.append('}'); + return sb.toString(); } public int describeContents() { diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index a0cdb63489c72e1e305a1088e2cb73d26843a624..343380cc766e63651b85112700f51baa68dfc49a 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -16,24 +16,24 @@ package android.app; +import static android.app.SuggestionsAdapter.getColumnString; + import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.content.res.Resources; -import android.content.res.Resources.NotFoundException; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemClock; import android.server.search.SearchableInfo; import android.speech.RecognizerIntent; @@ -45,7 +45,9 @@ import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; @@ -53,17 +55,15 @@ import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; import android.widget.Button; -import android.widget.CursorAdapter; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; -import android.widget.SimpleCursorAdapter; import android.widget.TextView; -import android.widget.WrapperListAdapter; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; -import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicLong; /** @@ -75,59 +75,74 @@ import java.util.concurrent.atomic.AtomicLong; public class SearchDialog extends Dialog implements OnItemClickListener, OnItemSelectedListener { // Debugging support - final static String LOG_TAG = "SearchDialog"; - private static final int DBG_LOG_TIMING = 0; - final static int DBG_JAM_THREADING = 0; + private static final boolean DBG = false; + private static final String LOG_TAG = "SearchDialog"; + private static final boolean DBG_LOG_TIMING = false; - // interaction with runtime - IntentFilter mCloseDialogsFilter; - IntentFilter mPackageFilter; - private static final String INSTANCE_KEY_COMPONENT = "comp"; private static final String INSTANCE_KEY_APPDATA = "data"; private static final String INSTANCE_KEY_GLOBALSEARCH = "glob"; private static final String INSTANCE_KEY_DISPLAY_QUERY = "dQry"; private static final String INSTANCE_KEY_DISPLAY_SEL_START = "sel1"; private static final String INSTANCE_KEY_DISPLAY_SEL_END = "sel2"; - private static final String INSTANCE_KEY_USER_QUERY = "uQry"; - private static final String INSTANCE_KEY_SUGGESTION_QUERY = "sQry"; private static final String INSTANCE_KEY_SELECTED_ELEMENT = "slEl"; private static final int INSTANCE_SELECTED_BUTTON = -2; private static final int INSTANCE_SELECTED_QUERY = -1; - + + private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12; + private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; + + // interaction with runtime + private IntentFilter mCloseDialogsFilter; + private IntentFilter mPackageFilter; + // views & widgets private TextView mBadgeLabel; - private AutoCompleteTextView mSearchTextField; + private ImageView mAppIcon; + private SearchAutoComplete mSearchAutoComplete; private Button mGoButton; private ImageButton mVoiceButton; + private View mSearchPlate; // interaction with searchable application + private SearchableInfo mSearchable; private ComponentName mLaunchComponent; private Bundle mAppSearchData; private boolean mGlobalSearchMode; private Context mActivityContext; - - // interaction with the search manager service - private SearchableInfo mSearchable; - // support for suggestions - private String mUserQuery = null; - private int mUserQuerySelStart; - private int mUserQuerySelEnd; - private boolean mLeaveJammedQueryOnRefocus = false; - private String mPreviousSuggestionQuery = null; - private int mPresetSelection = -1; - private String mSuggestionAction = null; - private Uri mSuggestionData = null; - private String mSuggestionQuery = null; + // Values we store to allow user to toggle between in-app search and global search. + private ComponentName mStoredComponentName; + private Bundle mStoredAppSearchData; + // stack of previous searchables, to support the BACK key after + // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE. + // The top of the stack (= previous searchable) is the last element of the list, + // since adding and removing is efficient at the end of an ArrayList. + private ArrayListtrue
if search dialog launched
+ */
+ private boolean show(ComponentName componentName, Bundle appSearchData,
+ boolean globalSearch) {
+
+ if (DBG) {
+ Log.d(LOG_TAG, "show(" + componentName + ", "
+ + appSearchData + ", " + globalSearch + ")");
+ }
+
+ // Try to get the searchable info for the provided component (or for global search,
+ // if globalSearch == true).
+ mSearchable = SearchManager.getSearchableInfo(componentName, globalSearch);
+
+ // If we got back nothing, and it wasn't a request for global search, then try again
+ // for global search, as we'll try to launch that in lieu of any component-specific search.
+ if (!globalSearch && mSearchable == null) {
+ globalSearch = true;
+ mSearchable = SearchManager.getSearchableInfo(componentName, globalSearch);
+
+ // If we still get back null (i.e., there's not even a searchable info available
+ // for global search), then really give up.
+ if (mSearchable == null) {
+ // Unfortunately, we can't log here. it would be logspam every time the user
+ // clicks the "search" key on a non-search app.
+ return false;
+ }
+ }
mLaunchComponent = componentName;
mAppSearchData = appSearchData;
- mGlobalSearchMode = globalSearch;
-
- // receive broadcasts
- getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
- getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
+ // Using globalSearch here is just an optimization, just calling
+ // isDefaultSearchable() should always give the same result.
+ mGlobalSearchMode = globalSearch || SearchManager.isDefaultSearchable(mSearchable);
+ mActivityContext = mSearchable.getActivityContext(getContext());
- // configure the autocomplete aspects of the input box
- mSearchTextField.setOnItemClickListener(this);
- mSearchTextField.setOnItemSelectedListener(this);
-
- // This conversion is necessary to force a preload of the EditText and thus force
- // suggestions to be presented (even for an empty query)
- if (initialQuery == null) {
- initialQuery = ""; // This forces the preload to happen, triggering suggestions
+ // show the dialog. this will call onStart().
+ if (!isShowing()) {
+ // First make sure the keyboard is showing (if needed), so that we get the right height
+ // for the dropdown to respect the IME.
+ if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
+ Configuration.HARDKEYBOARDHIDDEN_YES) {
+ InputMethodManager inputManager = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputManager.showSoftInputUnchecked(0, null);
+ }
+ show();
}
- // attach the suggestions adapter, if suggestions are available
- // The existence of a suggestions authority is the proxy for "suggestions available here"
- if (mSearchable.getSuggestAuthority() == null) {
- mSuggestionsAdapter = null;
- mSearchTextField.setAdapter(mSuggestionsAdapter);
- mSearchTextField.setText(initialQuery);
- } else {
- mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable,
- mSearchTextField);
- mSearchTextField.setAdapter(mSuggestionsAdapter);
-
- // finally, load the user's initial text (which may trigger suggestions)
- mSuggestionsAdapter.setNonUserQuery(false);
- mSearchTextField.setText(initialQuery);
- }
+ updateUI();
- if (selectInitialQuery) {
- mSearchTextField.selectAll();
- } else {
- mSearchTextField.setSelection(initialQuery.length());
- }
return true;
}
-
- /**
- * The default show() for this Dialog is not supported.
- */
+
@Override
- public void show() {
- return;
+ protected void onStart() {
+ super.onStart();
+
+ // receive broadcasts
+ getContext().registerReceiver(mBroadcastReceiver, mCloseDialogsFilter);
+ getContext().registerReceiver(mBroadcastReceiver, mPackageFilter);
}
/**
@@ -289,6 +372,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
public void onStop() {
super.onStop();
+ // TODO: Removing the listeners means that they never get called, since
+ // Dialog.dismissDialog() calls onStop() before sendDismissMessage().
setOnCancelListener(null);
setOnDismissListener(null);
@@ -299,26 +384,36 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
// This is OK - it just means we didn't have any registered
}
- // close any leftover cursor
- if (mSuggestionsAdapter != null) {
- mSuggestionsAdapter.changeCursor(null);
- }
+ closeSuggestionsAdapter();
// dump extra memory we're hanging on to
mLaunchComponent = null;
mAppSearchData = null;
mSearchable = null;
- mSuggestionAction = null;
- mSuggestionData = null;
- mSuggestionQuery = null;
mActivityContext = null;
- mPreviousSuggestionQuery = null;
mUserQuery = null;
+ mPreviousComponents = null;
+ }
+
+ /**
+ * Closes and gets rid of the suggestions adapter.
+ */
+ private void closeSuggestionsAdapter() {
+ // remove the adapter from the autocomplete first, to avoid any updates
+ // when we drop the cursor
+ mSearchAutoComplete.setAdapter((SuggestionsAdapter)null);
+ // close any leftover cursor
+ if (mSuggestionsAdapter != null) {
+ mSuggestionsAdapter.changeCursor(null);
+ }
+ mSuggestionsAdapter = null;
}
/**
* Save the minimal set of data necessary to recreate the search
*
+ * TODO: go through this and make sure that it saves everything that is needed
+ *
* @return A bundle with the state of the dialog.
*/
@Override
@@ -331,16 +426,14 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode);
// UI state
- bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchTextField.getText().toString());
- bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchTextField.getSelectionStart());
- bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchTextField.getSelectionEnd());
- bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
- bundle.putString(INSTANCE_KEY_SUGGESTION_QUERY, mPreviousSuggestionQuery);
+ bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchAutoComplete.getText().toString());
+ bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchAutoComplete.getSelectionStart());
+ bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchAutoComplete.getSelectionEnd());
int selectedElement = INSTANCE_SELECTED_QUERY;
if (mGoButton.isFocused()) {
selectedElement = INSTANCE_SELECTED_BUTTON;
- } else if (mSearchTextField.isPopupShowing()) {
+ } else if (mSearchAutoComplete.isPopupShowing()) {
selectedElement = 0; // TODO mSearchTextField.getListSelection() // 0..n
}
bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement);
@@ -350,6 +443,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
/**
* Restore the state of the dialog from a previously saved bundle.
+ *
+ * TODO: go through this and make sure that it saves everything that is saved
*
* @param savedInstanceState The state of the dialog previously saved by
* {@link #onSaveInstanceState()}.
@@ -365,26 +460,17 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
String displayQuery = savedInstanceState.getString(INSTANCE_KEY_DISPLAY_QUERY);
int querySelStart = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_START, -1);
int querySelEnd = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_END, -1);
- String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
int selectedElement = savedInstanceState.getInt(INSTANCE_KEY_SELECTED_ELEMENT);
- String suggestionQuery = savedInstanceState.getString(INSTANCE_KEY_SUGGESTION_QUERY);
// show the dialog. skip any show/hide animation, we want to go fast.
// send the text that actually generates the suggestions here; we'll replace the display
// text as necessary in a moment.
- if (!show(suggestionQuery, false, launchComponent, appSearchData, globalSearch)) {
+ if (!show(displayQuery, false, launchComponent, appSearchData, globalSearch)) {
// for some reason, we couldn't re-instantiate
return;
}
- if (mSuggestionsAdapter != null) {
- mSuggestionsAdapter.setNonUserQuery(true);
- }
- mSearchTextField.setText(displayQuery);
- // TODO because the new query is (not) processed in another thread, we can't just
- // take away this flag (yet). The better solution here is going to require a new API
- // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
-// mSuggestionsAdapter.setNonUserQuery(false);
+ mSearchAutoComplete.setText(displayQuery);
// clean up the selection state
switch (selectedElement) {
@@ -395,40 +481,38 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
break;
case INSTANCE_SELECTED_QUERY:
if (querySelStart >= 0 && querySelEnd >= 0) {
- mSearchTextField.requestFocus();
- mSearchTextField.setSelection(querySelStart, querySelEnd);
+ mSearchAutoComplete.requestFocus();
+ mSearchAutoComplete.setSelection(querySelStart, querySelEnd);
}
break;
default:
- // defer selecting a list element until suggestion list appears
- mPresetSelection = selectedElement;
- // TODO mSearchTextField.setListSelection(selectedElement)
+ // TODO: defer selecting a list element until suggestion list appears
+// mSearchAutoComplete.setListSelection(selectedElement)
break;
}
}
/**
- * Hook for updating layout on a rotation
- *
+ * Called after resources have changed, e.g. after screen rotation or locale change.
*/
public void onConfigurationChanged(Configuration newConfig) {
if (isShowing()) {
// Redraw (resources may have changed)
updateSearchButton();
+ updateSearchAppIcon();
updateSearchBadge();
updateQueryHint();
}
}
-
+
/**
- * Use SearchableInfo record (from search manager service) to preconfigure the UI in various
- * ways.
+ * Update the UI according to the info in the current value of {@link #mSearchable}.
*/
- private void setupSearchableInfo() {
+ private void updateUI() {
if (mSearchable != null) {
- mActivityContext = mSearchable.getActivityContext(getContext());
-
+ updateSearchAutoComplete();
updateSearchButton();
+ updateSearchAppIcon();
updateSearchBadge();
updateQueryHint();
updateVoiceButton();
@@ -449,24 +533,42 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
}
}
- mSearchTextField.setInputType(inputType);
- mSearchTextField.setImeOptions(mSearchable.getImeOptions());
+ mSearchAutoComplete.setInputType(inputType);
+ mSearchAutoComplete.setImeOptions(mSearchable.getImeOptions());
}
}
-
+
/**
- * The list of installed packages has just changed. This means that our current context
- * may no longer be valid. This would only happen if a package is installed/removed exactly
- * when the search bar is open. So for now we're just going to close the search
- * bar.
- *
- * Anything fancier would require some checks to see if the user's context was still valid.
- * Which would be messier.
+ * Updates the auto-complete text view.
*/
- public void onPackageListChange() {
- cancel();
+ private void updateSearchAutoComplete() {
+ // close any existing suggestions adapter
+ closeSuggestionsAdapter();
+
+ mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation
+ mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold());
+
+ if (mGlobalSearchMode) {
+ mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in
+ mSearchAutoComplete.setDropDownDismissedOnCompletion(false);
+ mSearchAutoComplete.setDropDownBackgroundResource(
+ com.android.internal.R.drawable.search_dropdown_background);
+ } else {
+ mSearchAutoComplete.setDropDownAlwaysVisible(false);
+ mSearchAutoComplete.setDropDownDismissedOnCompletion(true);
+ mSearchAutoComplete.setDropDownBackgroundResource(
+ com.android.internal.R.drawable.search_dropdown_background_apps);
+ }
+
+ // attach the suggestions adapter, if suggestions are available
+ // The existence of a suggestions authority is the proxy for "suggestions available here"
+ if (mSearchable.getSuggestAuthority() != null) {
+ mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable,
+ mOutsideDrawablesCache);
+ mSearchAutoComplete.setAdapter(mSuggestionsAdapter);
+ }
}
-
+
/**
* Update the text in the search button. Note: This is deprecated functionality, for
* 1.0 compatibility only.
@@ -481,26 +583,56 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
iconLabel = getContext().getResources().
getDrawable(com.android.internal.R.drawable.ic_btn_search);
}
- mGoButton.setText(textLabel);
+ mGoButton.setText(textLabel);
mGoButton.setCompoundDrawablesWithIntrinsicBounds(iconLabel, null, null, null);
}
+ private void updateSearchAppIcon() {
+ if (mGlobalSearchMode) {
+ 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 = null;
+ 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());
+ }
+ }
+
/**
- * Setup the search "Badge" if request by mode flags.
+ * Setup the search "Badge" if requested by mode flags.
*/
private void updateSearchBadge() {
// assume both hidden
int visibility = View.GONE;
Drawable icon = null;
- String text = null;
+ CharSequence text = null;
// optionally show one or the other.
- if (mSearchable.mBadgeIcon) {
+ if (mSearchable.useBadgeIcon()) {
icon = mActivityContext.getResources().getDrawable(mSearchable.getIconId());
visibility = View.VISIBLE;
- } else if (mSearchable.mBadgeLabel) {
+ if (DBG) Log.d(LOG_TAG, "Using badge icon: " + mSearchable.getIconId());
+ } else if (mSearchable.useBadgeLabel()) {
text = mActivityContext.getResources().getText(mSearchable.getLabelId()).toString();
visibility = View.VISIBLE;
+ if (DBG) Log.d(LOG_TAG, "Using badge label: " + mSearchable.getLabelId());
}
mBadgeLabel.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
@@ -520,7 +652,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
hint = mActivityContext.getString(hintId);
}
}
- mSearchTextField.setHint(hint);
+ mSearchAutoComplete.setHint(hint);
}
}
@@ -552,59 +684,84 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
* Listeners of various types
*/
+ /**
+ * {@link Dialog#onTouchEvent(MotionEvent)} will cancel the dialog only when the
+ * touch is outside the window. But the window includes space for the drop-down,
+ * so we also cancel on taps outside the search bar when the drop-down is not showing.
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // cancel if the drop-down is not showing and the touch event was outside the search plate
+ if (!mSearchAutoComplete.isPopupShowing() && isOutOfBounds(mSearchPlate, event)) {
+ if (DBG) Log.d(LOG_TAG, "Pop-up not showing and outside of search plate.");
+ cancel();
+ return true;
+ }
+ // Let Dialog handle events outside the window while the pop-up is showing.
+ return super.onTouchEvent(event);
+ }
+
+ private boolean isOutOfBounds(View v, MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
+ return (x < -slop) || (y < -slop)
+ || (x > (v.getWidth()+slop))
+ || (y > (v.getHeight()+slop));
+ }
+
/**
* Dialog's OnKeyListener implements various search-specific functionality
*
* @param keyCode This is the keycode of the typed key, and is the same value as
- * found in the KeyEvent parameter.
+ * found in the KeyEvent parameter.
* @param event The complete event record for the typed key
*
* @return Return true if the event was handled here, or false if not.
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- cancel();
+ if (DBG) Log.d(LOG_TAG, "onKeyDown(" + keyCode + "," + event + ")");
+
+ // handle back key to go back to previous searchable, etc.
+ if (handleBackKey(keyCode, event)) {
return true;
- case KeyEvent.KEYCODE_SEARCH:
- if (TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0) {
- launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
- } else {
- cancel();
- }
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+ // If the search key is pressed, toggle between global and in-app search. If we are
+ // currently doing global search and there is no in-app search context to toggle to,
+ // just don't do anything.
+ return toggleGlobalSearch();
+ }
+
+ // 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;
- default:
- SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
- if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
- launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
- return true;
- }
- break;
}
+
return false;
}
-
+
/**
* Callback to watch the textedit field for empty/non-empty
*/
private TextWatcher mTextWatcher = new TextWatcher() {
- public void beforeTextChanged(CharSequence s, int start, int
- before, int after) { }
+ public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
public void onTextChanged(CharSequence s, int start,
int before, int after) {
- if (DBG_LOG_TIMING == 1) {
+ if (DBG_LOG_TIMING) {
dbgLogTiming("onTextChanged()");
}
updateWidgetState();
- // Only do suggestions if actually typed by user
- if ((mSuggestionsAdapter != null) && !mSuggestionsAdapter.getNonUserQuery()) {
- mPreviousSuggestionQuery = s.toString();
- mUserQuery = mSearchTextField.getText().toString();
- mUserQuerySelStart = mSearchTextField.getSelectionStart();
- mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
+ if (!mSearchAutoComplete.isPerformingCompletion()) {
+ // The user changed the query, remember it.
+ mUserQuery = s == null ? "" : s.toString();
}
}
@@ -616,64 +773,34 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
private void updateWidgetState() {
// enable the button if we have one or more non-space characters
- boolean enabled =
- TextUtils.getTrimmedLength(mSearchTextField.getText()) != 0;
-
+ boolean enabled = !mSearchAutoComplete.isEmpty();
mGoButton.setEnabled(enabled);
mGoButton.setFocusable(enabled);
}
- private final static String[] ONE_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1 };
- private final static String[] ONE_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_ICON_1,
- SearchManager.SUGGEST_COLUMN_ICON_2};
- private final static String[] TWO_LINE_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_TEXT_2 };
- private final static String[] TWO_LINE_ICONS_FROM = {SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_TEXT_2,
- SearchManager.SUGGEST_COLUMN_ICON_1,
- SearchManager.SUGGEST_COLUMN_ICON_2 };
-
- private final static int[] ONE_LINE_TO = {com.android.internal.R.id.text1};
- private final static int[] ONE_LINE_ICONS_TO = {com.android.internal.R.id.text1,
- com.android.internal.R.id.icon1,
- com.android.internal.R.id.icon2};
- private final static int[] TWO_LINE_TO = {com.android.internal.R.id.text1,
- com.android.internal.R.id.text2};
- private final static int[] TWO_LINE_ICONS_TO = {com.android.internal.R.id.text1,
- com.android.internal.R.id.text2,
- com.android.internal.R.id.icon1,
- com.android.internal.R.id.icon2};
-
- /**
- * Safely retrieve the suggestions cursor adapter from the ListView
- *
- * @param adapterView The ListView containing our adapter
- * @result The CursorAdapter that we installed, or null if not set
- */
- private static CursorAdapter getSuggestionsAdapter(AdapterView> adapterView) {
- CursorAdapter result = null;
- if (adapterView != null) {
- Object ad = adapterView.getAdapter();
- if (ad instanceof CursorAdapter) {
- result = (CursorAdapter) ad;
- } else if (ad instanceof WrapperListAdapter) {
- result = (CursorAdapter) ((WrapperListAdapter)ad).getWrappedAdapter();
- }
- }
- return result;
- }
-
/**
* React to typing in the GO search button by refocusing to EditText.
* Continue typing the query.
*/
View.OnKeyListener mButtonsKeyListener = new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null) {
- return refocusingKeyListener(v, keyCode, event);
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return false;
}
+
+ if (!event.isSystem() &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
+ (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
+ // restore focus and give key to EditText ...
+ if (mSearchAutoComplete.requestFocus()) {
+ return mSearchAutoComplete.dispatchKeyEvent(event);
+ }
+ }
+
return false;
}
};
@@ -683,10 +810,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
View.OnClickListener mGoButtonClickListener = new View.OnClickListener() {
public void onClick(View v) {
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null) {
- launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return;
}
+ launchQuerySearch();
}
};
@@ -695,14 +823,16 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
View.OnClickListener mVoiceButtonClickListener = new View.OnClickListener() {
public void onClick(View v) {
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return;
+ }
try {
if (mSearchable.getVoiceSearchLaunchWebSearch()) {
getContext().startActivity(mVoiceWebSearchIntent);
- dismiss();
} else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent);
getContext().startActivity(appSearchIntent);
- dismiss();
}
} catch (ActivityNotFoundException e) {
// Should not happen, since we check the availability of
@@ -724,7 +854,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
// 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(mSearchable.mSearchActivity);
+ queryIntent.setComponent(mSearchable.getSearchActivity());
PendingIntent pending = PendingIntent.getActivity(
getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
@@ -778,136 +908,56 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*/
View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- cancel();
- return true;
- }
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null &&
- TextUtils.getTrimmedLength(mSearchTextField.getText()) > 0) {
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("doTextKey()");
- }
- // dispatch "typing in the list" first
- if (mSearchTextField.isPopupShowing() &&
- mSearchTextField.getListSelection() != ListView.INVALID_POSITION) {
- return onSuggestionsKey(v, keyCode, event);
- }
- // otherwise, dispatch an "edit view" key
- switch (keyCode) {
- case KeyEvent.KEYCODE_ENTER:
- if (event.getAction() == KeyEvent.ACTION_UP) {
- v.cancelLongPress();
- launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
- return true;
- }
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- // capture the EditText state, so we can restore the user entry later
- mUserQuery = mSearchTextField.getText().toString();
- mUserQuerySelStart = mSearchTextField.getSelectionStart();
- mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
- // pass through - we're just watching here
- break;
- default:
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
- if ((actionKey != null) && (actionKey.mQueryActionMsg != null)) {
- launchQuerySearch(keyCode, actionKey.mQueryActionMsg);
- return true;
- }
- }
- break;
- }
+ // guard against possible race conditions
+ if (mSearchable == null) {
+ return false;
}
- return false;
- }
- };
-
- /**
- * React to the user typing while the suggestions are focused. First, check for action
- * keys. If not handled, try refocusing regular characters into the EditText. In this case,
- * replace the query text (start typing fresh text).
- */
- private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
- boolean handled = false;
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable != null) {
- handled = doSuggestionsKey(v, keyCode, event);
- }
- return handled;
- }
-
- /**
- * Per UI design, we're going to "steer" any typed keystrokes back into the EditText
- * box, even if the user has navigated the focus to the dropdown or to the GO button.
- *
- * @param v The view into which the keystroke was typed
- * @param keyCode keyCode of entered key
- * @param event Full KeyEvent record of entered key
- */
- private boolean refocusingKeyListener(View v, int keyCode, KeyEvent event) {
- boolean handled = false;
- if (!event.isSystem() &&
- (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
- (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
- (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
- (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
- (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
- // restore focus and give key to EditText ...
- // but don't replace the user's query
- mLeaveJammedQueryOnRefocus = true;
- if (mSearchTextField.requestFocus()) {
- handled = mSearchTextField.dispatchKeyEvent(event);
+ if (DBG_LOG_TIMING) dbgLogTiming("doTextKey()");
+ if (DBG) {
+ Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event
+ + "), selection: " + mSearchAutoComplete.getListSelection());
}
- mLeaveJammedQueryOnRefocus = false;
- }
- return handled;
- }
-
- /**
- * Update query text based on transitions in and out of suggestions list.
- */
- /*
- * TODO - figure out if this logic is required for the autocomplete text view version
-
- OnFocusChangeListener mSuggestFocusListener = new OnFocusChangeListener() {
- public void onFocusChange(View v, boolean hasFocus) {
- // also guard against possible race conditions (late arrival after dismiss)
- if (mSearchable == null) {
- return;
+
+ // If a suggestion is selected, handle enter, search key, and action keys
+ // as presses on the selected suggestion
+ if (mSearchAutoComplete.isPopupShowing() &&
+ mSearchAutoComplete.getListSelection() != ListView.INVALID_POSITION) {
+ return onSuggestionsKey(v, keyCode, event);
}
- // Update query text based on navigation in to/out of the suggestions list
- if (hasFocus) {
- // Entering the list view - record selection point from user's query
- mUserQuery = mSearchTextField.getText().toString();
- mUserQuerySelStart = mSearchTextField.getSelectionStart();
- mUserQuerySelEnd = mSearchTextField.getSelectionEnd();
- // then update the query to match the entered selection
- jamSuggestionQuery(true, mSuggestionsList,
- mSuggestionsList.getSelectedItemPosition());
- } else {
- // Exiting the list view
-
- if (mSuggestionsList.getSelectedItemPosition() < 0) {
- // Direct exit - Leave new suggestion in place (do nothing)
- } else {
- // Navigation exit - restore user's query text
- if (!mLeaveJammedQueryOnRefocus) {
- jamSuggestionQuery(false, null, -1);
+
+ // If there is text in the query box, handle enter, and action keys
+ // The search key is handled by the dialog's onKeyDown().
+ if (!mSearchAutoComplete.isEmpty()) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ v.cancelLongPress();
+ launchQuerySearch();
+ return true;
+ }
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+ if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
+ launchQuerySearch(keyCode, actionKey.getQueryActionMsg());
+ return true;
}
}
}
-
+ return false;
}
};
- */
-
+
/**
- * This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent. It's an indication that
- * we should close ourselves immediately, in order to allow a higher-priority UI to take over
+ * When the ACTION_CLOSE_SYSTEM_DIALOGS intent is received, we should close ourselves
+ * immediately, in order to allow a higher-priority UI to take over
* (e.g. phone call received).
+ *
+ * When a package is added, removed or changed, our current context
+ * may no longer be valid. This would only happen if a package is installed/removed exactly
+ * when the search bar is open. So for now we're just going to close the search
+ * bar.
+ * Anything fancier would require some checks to see if the user's context was still valid.
+ * Which would be messier.
*/
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -918,7 +968,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)
|| Intent.ACTION_PACKAGE_REMOVED.equals(action)
|| Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
- onPackageListChange();
+ cancel();
}
}
};
@@ -938,58 +988,45 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
/**
- * Various ways to launch searches
+ * React to the user typing while in the suggestions list. First, check for action
+ * keys. If not handled, try refocusing regular characters into the EditText.
*/
-
- /**
- * React to the user clicking the "GO" button. Hide the UI and launch a search.
- *
- * @param actionKey Pass a keycode if the launch was triggered by an action key. Pass
- * KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
- * @param actionMsg Pass the suggestion-provided message if the launch was triggered by an
- * action key. Pass null for no actionKey message.
- */
- private void launchQuerySearch(int actionKey, final String actionMsg) {
- final String query = mSearchTextField.getText().toString();
- final Bundle appData = mAppSearchData;
- final SearchableInfo si = mSearchable; // cache briefly (dismiss() nulls it)
- dismiss();
- sendLaunchIntent(Intent.ACTION_SEARCH, null, query, appData, actionKey, actionMsg, si);
- }
-
- /**
- * React to the user typing an action key while in the suggestions list
- */
- private boolean doSuggestionsKey(View v, int keyCode, KeyEvent event) {
- // Exit early in case of race condition
+ private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
+ // guard against possible race conditions (late arrival after dismiss)
+ if (mSearchable == null) {
+ return false;
+ }
if (mSuggestionsAdapter == null) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
- if (DBG_LOG_TIMING == 1) {
- dbgLogTiming("doSuggestionsKey()");
+ if (DBG_LOG_TIMING) {
+ dbgLogTiming("onSuggestionsKey()");
}
// First, check for enter or search (both of which we'll treat as a "click")
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
- int position = mSearchTextField.getListSelection();
- return launchSuggestion(mSuggestionsAdapter, position);
+ int position = mSearchAutoComplete.getListSelection();
+ return launchSuggestion(position);
}
// Next, check for left/right moves, which we use to "return" the user to the edit view
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
- // give "focus" to text editor, but don't restore the user's original query
+ // give "focus" to text editor, with cursor at the beginning if
+ // left key, at end if right key
+ // TODO: Reverse left/right for right-to-left languages, e.g. Arabic
int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ?
- 0 : mSearchTextField.length();
- mSearchTextField.setSelection(selPoint);
- mSearchTextField.setListSelection(0);
- mSearchTextField.clearListSelection();
+ 0 : mSearchAutoComplete.length();
+ mSearchAutoComplete.setSelection(selPoint);
+ mSearchAutoComplete.setListSelection(0);
+ mSearchAutoComplete.clearListSelection();
return true;
}
// Next, check for an "up and out" move
- if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchTextField.getListSelection()) {
- jamSuggestionQuery(false, null, -1);
+ if (keyCode == KeyEvent.KEYCODE_DPAD_UP
+ && 0 == mSearchAutoComplete.getListSelection()) {
+ restoreUserQuery();
// let ACTV complete the move
return false;
}
@@ -997,160 +1034,196 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
// Next, check for an "action key"
SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
if ((actionKey != null) &&
- ((actionKey.mSuggestActionMsg != null) ||
- (actionKey.mSuggestActionMsgColumn != null))) {
- // launch suggestion using action key column
- int position = mSearchTextField.getListSelection();
- if (position >= 0) {
+ ((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)) {
- // shut down search bar and launch the activity
- // cache everything we need because dismiss releases mems
- setupSuggestionIntent(c, mSearchable);
- final String query = mSearchTextField.getText().toString();
- final Bundle appData = mAppSearchData;
- SearchableInfo si = mSearchable;
- String suggestionAction = mSuggestionAction;
- Uri suggestionData = mSuggestionData;
- String suggestionQuery = mSuggestionQuery;
- dismiss();
- sendLaunchIntent(suggestionAction, suggestionData,
- suggestionQuery, appData,
- keyCode, actionMsg, si);
- return true;
+ return launchSuggestion(position, keyCode, actionMsg);
}
}
}
}
}
return false;
- }
+ }
+
+ /**
+ * Launch a search for the text in the query text field.
+ */
+ protected void launchQuerySearch() {
+ launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null);
+ }
/**
- * Set or reset the user query to follow the selections in the suggestions
+ * Launch a search for the text in the query text field.
+ *
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or null
if none.
+ */
+ protected void launchQuerySearch(int actionKey, String actionMsg) {
+ String query = mSearchAutoComplete.getText().toString();
+ Intent intent = createIntent(Intent.ACTION_SEARCH, null, query, null,
+ actionKey, actionMsg);
+ launchIntent(intent);
+ }
+
+ /**
+ * Launches an intent based on a suggestion.
*
- * @param jamQuery True means to set the query, false means to reset it to the user's choice
+ * @param position The index of the suggestion to create the intent from.
+ * @return true if a successful launch, false if could not (e.g. bad position).
*/
- private void jamSuggestionQuery(boolean jamQuery, AdapterView> parent, int position) {
- // quick check against race conditions
- if (mSearchable == null) {
+ protected boolean launchSuggestion(int position) {
+ return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
+ }
+
+ /**
+ * Launches an intent based on a suggestion.
+ *
+ * @param position The index of the suggestion to create the intent from.
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or null
if none.
+ * @return true if a successful launch, false if could not (e.g. bad position).
+ */
+ protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
+ Cursor c = mSuggestionsAdapter.getCursor();
+ if ((c != null) && c.moveToPosition(position)) {
+ Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
+ launchIntent(intent);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Launches an intent. Also dismisses the search dialog if not in global search mode.
+ */
+ private void launchIntent(Intent intent) {
+ if (intent == null) {
return;
}
-
- mSuggestionsAdapter.setNonUserQuery(true); // disables any suggestions processing
- if (jamQuery) {
- CursorAdapter ca = getSuggestionsAdapter(parent);
- Cursor c = ca.getCursor();
- if (c.moveToPosition(position)) {
- setupSuggestionIntent(c, mSearchable);
- String jamText = null;
-
- // Simple heuristic for selecting text with which to rewrite the query.
- if (mSuggestionQuery != null) {
- jamText = mSuggestionQuery;
- } else if (mSearchable.mQueryRewriteFromData && (mSuggestionData != null)) {
- jamText = mSuggestionData.toString();
- } else if (mSearchable.mQueryRewriteFromText) {
- try {
- int column = c.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1);
- jamText = c.getString(column);
- } catch (RuntimeException e) {
- // no work here, jamText is null
- }
- }
- if (jamText != null) {
- mSearchTextField.setText(jamText);
- /* mSearchTextField.selectAll(); */ // this didn't work anyway in the old UI
- // TODO this is only needed in the model where we have a selection in the ACTV
- // and in the dropdown at the same time.
- mSearchTextField.setSelection(jamText.length());
- }
- }
- } else {
- // reset user query
- mSearchTextField.setText(mUserQuery);
- try {
- mSearchTextField.setSelection(mUserQuerySelStart, mUserQuerySelEnd);
- } catch (IndexOutOfBoundsException e) {
- // In case of error, just select all
- Log.e(LOG_TAG, "Caught IndexOutOfBoundsException while setting selection. " +
- "start=" + mUserQuerySelStart + " end=" + mUserQuerySelEnd +
- " text=\"" + mUserQuery + "\"");
- mSearchTextField.selectAll();
- }
+ if (handleSpecialIntent(intent)){
+ return;
}
- // TODO because the new query is (not) processed in another thread, we can't just
- // take away this flag (yet). The better solution here is going to require a new API
- // in AutoCompleteTextView which allows us to change the text w/o changing the suggestions.
-// mSuggestionsAdapter.setNonUserQuery(false);
+ if (!mGlobalSearchMode) {
+ dismiss();
+ }
+ getContext().startActivity(intent);
}
-
+
/**
- * Assemble a search intent and send it.
- *
- * @param action The intent to send, typically Intent.ACTION_SEARCH
- * @param data The data for the intent
- * @param query The user text entered (so far)
- * @param appData The app data bundle (if supplied)
- * @param actionKey If the intent was triggered by an action key, e.g. KEYCODE_CALL, it will
- * be sent here. Pass KeyEvent.KEYCODE_UNKNOWN for no actionKey code.
- * @param actionMsg If the intent was triggered by an action key, e.g. KEYCODE_CALL, the
- * corresponding tag message will be sent here. Pass null for no actionKey message.
- * @param si Reference to the current SearchableInfo. Passed here so it can be used even after
- * we've called dismiss(), which attempts to null mSearchable.
+ * Handles the special intent actions declared in {@link SearchManager}.
+ *
+ * @return true
if the intent was handled.
*/
- private void sendLaunchIntent(final String action, final Uri data, final String query,
- final Bundle appData, int actionKey, final String actionMsg, final SearchableInfo si) {
- Intent launcher = new Intent(action);
-
- if (query != null) {
- launcher.putExtra(SearchManager.QUERY, query);
+ private boolean handleSpecialIntent(Intent intent) {
+ String action = intent.getAction();
+ if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) {
+ handleChangeSourceIntent(intent);
+ return true;
+ } else if (SearchManager.INTENT_ACTION_CURSOR_RESPOND.equals(action)) {
+ handleCursorRespondIntent(intent);
+ return true;
}
-
- if (data != null) {
- launcher.setData(data);
+ return false;
+ }
+
+ /**
+ * Handles SearchManager#INTENT_ACTION_CHANGE_SOURCE.
+ */
+ private void handleChangeSourceIntent(Intent intent) {
+ Uri dataUri = intent.getData();
+ if (dataUri == null) {
+ Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data.");
+ return;
}
-
- if (appData != null) {
- launcher.putExtra(SearchManager.APP_DATA, appData);
+ ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString());
+ if (componentName == null) {
+ Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri);
+ return;
}
-
- // add launch info (action key, etc.)
- if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
- launcher.putExtra(SearchManager.ACTION_KEY, actionKey);
- launcher.putExtra(SearchManager.ACTION_MSG, actionMsg);
+ if (DBG) Log.d(LOG_TAG, "Switching to " + componentName);
+
+ ComponentName previous = mLaunchComponent;
+ if (!show(componentName, mAppSearchData, false)) {
+ Log.w(LOG_TAG, "Failed to switch to source " + componentName);
+ return;
}
+ pushPreviousComponent(previous);
- // attempt to enforce security requirement (no 3rd-party intents)
- launcher.setComponent(si.mSearchActivity);
-
- getContext().startActivity(launcher);
+ String query = intent.getStringExtra(SearchManager.QUERY);
+ setUserQuery(query);
}
-
+
/**
- * Shared code for launching a query from a suggestion.
- * @param ca The cursor adapter containing the suggestions
- * @param position The suggestion we'll be launching from
- * @return true if a successful launch, false if could not (e.g. bad position)
+ * Handles {@link SearchManager#INTENT_ACTION_CURSOR_RESPOND}.
*/
- private boolean launchSuggestion(CursorAdapter ca, int position) {
- Cursor c = ca.getCursor();
- if ((c != null) && c.moveToPosition(position)) {
- setupSuggestionIntent(c, mSearchable);
-
- final Bundle appData = mAppSearchData;
- SearchableInfo si = mSearchable;
- String suggestionAction = mSuggestionAction;
- Uri suggestionData = mSuggestionData;
- String suggestionQuery = mSuggestionQuery;
- dismiss();
- sendLaunchIntent(suggestionAction, suggestionData, suggestionQuery, appData,
- KeyEvent.KEYCODE_UNKNOWN, null, si);
- return true;
+ private void handleCursorRespondIntent(Intent intent) {
+ Cursor c = mSuggestionsAdapter.getCursor();
+ if (c != null) {
+ c.respond(intent.getExtras());
}
- return false;
+ }
+
+ /**
+ * Saves the previous component that was searched, so that we can go
+ * back to it.
+ */
+ private void pushPreviousComponent(ComponentName componentName) {
+ if (mPreviousComponents == null) {
+ mPreviousComponents = new ArrayListnull
if there was
+ * no previous component.
+ */
+ private ComponentName popPreviousComponent() {
+ if (mPreviousComponents == null) {
+ return null;
+ }
+ int size = mPreviousComponents.size();
+ if (size == 0) {
+ return null;
+ }
+ return mPreviousComponents.remove(size - 1);
+ }
+
+ /**
+ * Goes back to the previous component that was searched, if any.
+ *
+ * @return true
if there was a previous component that we could go back to.
+ */
+ private boolean backToPreviousComponent() {
+ ComponentName previous = popPreviousComponent();
+ if (previous == null) {
+ return false;
+ }
+ if (!show(previous, mAppSearchData, false)) {
+ Log.w(LOG_TAG, "Failed to switch to source " + previous);
+ return false;
+ }
+
+ // must touch text to trigger suggestions
+ // TODO: should this be the text as it was when the user left
+ // the source that we are now going back to?
+ String query = mSearchAutoComplete.getText().toString();
+ setUserQuery(query);
+
+ return true;
}
/**
@@ -1159,62 +1232,43 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
* and/or falling back to the XML for defaults; It also creates REST style Uri data when
* the suggestion includes a data id.
*
- * NOTE: Return values are in member variables mSuggestionAction & mSuggestionData.
- *
* @param c The suggestions cursor, moved to the row of the user's selection
- * @param si The searchable activity's info record
+ * @param actionKey The key code of the action key that was pressed,
+ * or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+ * @param actionMsg The message for the action key that was pressed,
+ * or null
if none.
+ * @return An intent for the suggestion at the cursor's position.
*/
- void setupSuggestionIntent(Cursor c, SearchableInfo si) {
+ private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
try {
// use specific action if supplied, or default action if supplied, or fixed default
- mSuggestionAction = null;
- int mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
- if (mColumn >= 0) {
- final String action = c.getString(mColumn);
- if (action != null) {
- mSuggestionAction = action;
- }
- }
- if (mSuggestionAction == null) {
- mSuggestionAction = si.getSuggestIntentAction();
+ String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
+ if (action == null) {
+ action = mSearchable.getSuggestIntentAction();
}
- if (mSuggestionAction == null) {
- mSuggestionAction = Intent.ACTION_SEARCH;
+ if (action == null) {
+ action = Intent.ACTION_SEARCH;
}
// use specific data if supplied, or default data if supplied
- String data = null;
- mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
- if (mColumn >= 0) {
- final String rowData = c.getString(mColumn);
- if (rowData != null) {
- data = rowData;
- }
- }
+ String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
if (data == null) {
- data = si.getSuggestIntentData();
+ data = mSearchable.getSuggestIntentData();
}
-
// then, if an ID was provided, append it.
if (data != null) {
- mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
- if (mColumn >= 0) {
- final String id = c.getString(mColumn);
- if (id != null) {
- data = data + "/" + Uri.encode(id);
- }
+ String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
+ if (id != null) {
+ data = data + "/" + Uri.encode(id);
}
}
- mSuggestionData = (data == null) ? null : Uri.parse(data);
+ Uri dataUri = (data == null) ? null : Uri.parse(data);
+
+ String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
- mSuggestionQuery = null;
- mColumn = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY);
- if (mColumn >= 0) {
- final String query = c.getString(mColumn);
- if (query != null) {
- mSuggestionQuery = query;
- }
- }
+ String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
+
+ return createIntent(action, dataUri, query, extraData, actionKey, actionMsg);
} catch (RuntimeException e ) {
int rowNum;
try { // be really paranoid now
@@ -1224,7 +1278,46 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
}
Log.w(LOG_TAG, "Search Suggestions cursor at row " + rowNum +
" returned exception" + e.toString());
+ return null;
+ }
+ }
+
+ /**
+ * Constructs an intent from the given information and the search dialog state.
+ *
+ * @param action Intent action.
+ * @param data Intent data, or null
.
+ * @param query Intent query, or null
.
+ * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or null
.
+ * @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 null
if none.
+ * @return The intent.
+ */
+ private Intent createIntent(String action, Uri data, String query, String extraData,
+ int actionKey, String actionMsg) {
+ // Now build the Intent
+ Intent intent = new Intent(action);
+ if (data != null) {
+ intent.setData(data);
+ }
+ if (query != null) {
+ intent.putExtra(SearchManager.QUERY, query);
+ }
+ if (extraData != null) {
+ intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+ }
+ if (mAppSearchData != null) {
+ intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
+ }
+ if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
+ intent.putExtra(SearchManager.ACTION_KEY, actionKey);
+ intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
}
+ // attempt to enforce security requirement (no 3rd-party intents)
+ intent.setComponent(mSearchable.getSearchActivity());
+ return intent;
}
/**
@@ -1236,364 +1329,195 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
*
* @return Returns a string, or null if no action key message for this suggestion
*/
- private String getActionKeyMessage(Cursor c, final SearchableInfo.ActionKeyInfo actionKey) {
+ private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
String result = null;
// check first in the cursor data, for a suggestion-specific message
- final String column = actionKey.mSuggestActionMsgColumn;
+ final String column = actionKey.getSuggestActionMsgColumn();
if (column != null) {
- try {
- int colId = c.getColumnIndexOrThrow(column);
- result = c.getString(colId);
- } catch (RuntimeException e) {
- // OK - result is already null
- }
+ result = SuggestionsAdapter.getColumnString(c, column);
}
// If the cursor didn't give us a message, see if there's a single message defined
// for the actionkey (for all suggestions)
if (result == null) {
- result = actionKey.mSuggestActionMsg;
+ result = actionKey.getSuggestActionMsg();
}
return result;
}
/**
- * Local subclass for AutoCompleteTextView
- *
- * This exists entirely to override the threshold method. Otherwise we just use the class
- * as-is.
+ * Local subclass for AutoCompleteTextView.
*/
public static class SearchAutoComplete extends AutoCompleteTextView {
+ private int mThreshold;
+ private SearchDialog mSearchDialog;
+
public SearchAutoComplete(Context context) {
- super(null);
+ 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();
}
-
- /**
- * We never allow ACTV to automatically replace the text, since we use "jamSuggestionQuery"
- * to do that. There's no point in letting ACTV do this here, because in the search UI,
- * as soon as we click a suggestion, we're going to start shutting things down.
- */
- @Override
- public void replaceText(CharSequence text) {
+
+ private void setSearchDialog(SearchDialog searchDialog) {
+ mSearchDialog = searchDialog;
}
- /**
- * We always return true, so that the effective threshold is "zero". This allows us
- * to provide "null" suggestions such as "just show me some recent entries".
- */
@Override
- public boolean enoughToFilter() {
- return true;
+ public void setThreshold(int threshold) {
+ super.setThreshold(threshold);
+ mThreshold = threshold;
}
- }
-
- /**
- * Support for AutoCompleteTextView-based suggestions
- */
- /**
- * This class provides the filtering-based interface to suggestions providers.
- * It is hardwired in a couple of places to support GoogleSearch - for example, it supports
- * two-line suggestions, but it does not support icons.
- */
- private static class SuggestionsAdapter extends SimpleCursorAdapter {
- private final String TAG = "SuggestionsAdapter";
-
- SearchableInfo mSearchable;
- private Resources mProviderResources;
-
- // These private variables are shared by the filter thread and must be protected
- private WeakReferenceAdditional Metadata for search suggestions. If you have defined a content provider + *
Additional metadata for search suggestions. If you have defined a content provider * to generate search suggestions, you'll need to publish it to the system, and you'll need to * provide a bit of additional XML metadata in order to configure communications with it. * @@ -880,7 +885,7 @@ import android.view.KeyEvent; * * * - *
Additional Metadata for search action keys. For each action key that you would like to + *
Additional metadata for search action keys. For each action key that you would like to * define, you'll need to add an additional element defining that key, and using the attributes * discussed in Action Keys. A simple example is shown here: * @@ -956,6 +961,84 @@ import android.view.KeyEvent; * * * + *
Additional metadata for enabling voice search. To enable voice search for your + * activity, you can add fields to the metadata that enable and configure voice search. When + * enabled (and available on the device), a voice search button will be displayed in the + * Search UI. Clicking this button will launch a voice search activity. When the user has + * finished speaking, the voice search phrase will be transcribed into text and presented to the + * searchable activity as if it were a typed query. + * + *
Elements of search metadata that support voice search: + *
Attribute | Description | Required? | ||||||
---|---|---|---|---|---|---|---|---|
android:voiceSearchMode | + *If provided and non-zero, enables voice search. (Voice search may not be
+ * provided by the device, in which case these flags will have no effect.) The
+ * following mode bits are defined:
+ *
|
+ * No | + *||||||
android:voiceLanguageModel | + *If provided, this specifies the language model that should be used by the voice + * recognition system. + * See {@link android.speech.RecognizerIntent#EXTRA_LANGUAGE_MODEL} + * for more information. If not provided, the default value + * {@link android.speech.RecognizerIntent#LANGUAGE_MODEL_FREE_FORM} will be used. | + *No | + *||||||
android:voicePromptText | + *If provided, this specifies a prompt that will be displayed during voice input. + * (If not provided, a default prompt will be displayed.) | + *No | + *||||||
android:voiceLanguage | + *If provided, this specifies the spoken language to be expected. This is only + * needed if it is different from the current value of + * {@link java.util.Locale#getDefault()}. + * | + *No | + *||||||
android:voiceMaxResults | + *If provided, enforces the maximum number of results to return, including the "best" + * result which will always be provided as the SEARCH intent's primary query. Must be + * one or greater. Use {@link android.speech.RecognizerIntent#EXTRA_RESULTS} + * to get the results from the intent. If not provided, the recognizer will choose + * how many results to return. | + *No | + *
false
, return information about the given activity.
+ * If true
, return information about the global search activity.
+ * @return Searchable information, or null
if the activity is not searchable.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public static SearchableInfo getSearchableInfo(ComponentName componentName,
+ boolean globalSearch) {
+ try {
+ return sService.getSearchableInfo(componentName, globalSearch);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Checks whether the given searchable is the default searchable.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public static boolean isDefaultSearchable(SearchableInfo searchable) {
+ SearchableInfo defaultSearchable = SearchManager.getSearchableInfo(null, true);
+ return defaultSearchable != null
+ && defaultSearchable.getSearchActivity().equals(searchable.getSearchActivity());
+ }
+
+ /**
+ * Gets a cursor with search suggestions. This method is static so that it can
+ * be used from non-Activity context.
+ *
+ * @param searchable Information about how to get the suggestions.
+ * @param query The search text entered (so far).
+ * @return a cursor with suggestions, or null the suggestion query failed.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public static Cursor getSuggestions(Context context, SearchableInfo searchable, String query) {
+ if (searchable == null) {
+ return null;
+ }
+
+ String authority = searchable.getSuggestAuthority();
+ if (authority == null) {
+ return null;
+ }
+
+ Uri.Builder uriBuilder = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority);
+
+ // if content path provided, insert it now
+ final String contentPath = searchable.getSuggestPath();
+ if (contentPath != null) {
+ uriBuilder.appendEncodedPath(contentPath);
+ }
+
+ // append standard suggestion query path
+ uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
+
+ // get the query selection, may be null
+ String selection = searchable.getSuggestSelection();
+ // inject query, either as selection args or inline
+ String[] selArgs = null;
+ if (selection != null) { // use selection if provided
+ selArgs = new String[] { query };
+ } else { // no selection, use REST pattern
+ uriBuilder.appendPath(query);
+ }
+
+ Uri uri = uriBuilder
+ .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
+ .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel()
+ .build();
+
+ // finally, make the query
+ return context.getContentResolver().query(uri, null, selection, selArgs, null);
+ }
+
+ /**
+ * Returns a list of the searchable activities that can be included in global search.
+ *
+ * @return a list containing searchable information for all searchable activities
+ * that have the exported
attribute set in their searchable
+ * meta-data.
+ *
+ * @hide because SearchableInfo is not part of the API.
+ */
+ public static List getSearchablesInGlobalSearch() {
+ try {
+ return sService.getSearchablesInGlobalSearch();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
}
diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..6a02fc930a1c3bdf1a45e3fe72398459dbd4dbc1
--- /dev/null
+++ b/core/java/android/app/SuggestionsAdapter.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.server.search.SearchableInfo;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+import java.io.FileNotFoundException;
+import java.util.WeakHashMap;
+
+/**
+ * Provides the contents for the suggestion drop-down list.in {@link SearchDialog}.
+ *
+ * @hide
+ */
+class SuggestionsAdapter extends ResourceCursorAdapter {
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "SuggestionsAdapter";
+
+ private SearchableInfo mSearchable;
+ private Context mProviderContext;
+ private WeakHashMap mOutsideDrawablesCache;
+
+ // Cached column indexes, updated when the cursor changes.
+ private int mFormatCol;
+ private int mText1Col;
+ private int mText2Col;
+ private int mIconName1Col;
+ private int mIconName2Col;
+ private int mIconBitmap1Col;
+ private int mIconBitmap2Col;
+
+ public SuggestionsAdapter(Context context, SearchableInfo searchable,
+ WeakHashMap outsideDrawablesCache) {
+ super(context,
+ com.android.internal.R.layout.search_dropdown_item_icons_2line,
+ null, // no initial cursor
+ true); // auto-requery
+ mSearchable = searchable;
+
+ // set up provider resources (gives us icons, etc.)
+ Context activityContext = mSearchable.getActivityContext(mContext);
+ mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
+
+ mOutsideDrawablesCache = outsideDrawablesCache;
+ }
+
+ /**
+ * Overridden to always return false
, since we cannot be sure that
+ * suggestion sources return stable IDs.
+ */
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ /**
+ * Use the search suggestions provider to obtain a live cursor. This will be called
+ * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
+ * The results will be processed in the UI thread and changeCursor() will be called.
+ */
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
+ String query = (constraint == null) ? "" : constraint.toString();
+ try {
+ return SearchManager.getSuggestions(mContext, mSearchable, query);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Cache columns.
+ */
+ @Override
+ public void changeCursor(Cursor c) {
+ if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
+ super.changeCursor(c);
+ if (c != null) {
+ mFormatCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT);
+ mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
+ mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
+ mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
+ mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
+ mIconBitmap1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1_BITMAP);
+ mIconBitmap2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2_BITMAP);
+ }
+ }
+
+ /**
+ * Tags the view with cached child view look-ups.
+ */
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View v = super.newView(context, cursor, parent);
+ v.setTag(new ChildViewCache(v));
+ return v;
+ }
+
+ /**
+ * Cache of the child views of drop-drown list items, to avoid looking up the children
+ * each time the contents of a list item are changed.
+ */
+ private final static class ChildViewCache {
+ public final TextView mText1;
+ public final TextView mText2;
+ public final ImageView mIcon1;
+ public final ImageView mIcon2;
+
+ public ChildViewCache(View v) {
+ mText1 = (TextView) v.findViewById(com.android.internal.R.id.text1);
+ mText2 = (TextView) v.findViewById(com.android.internal.R.id.text2);
+ mIcon1 = (ImageView) v.findViewById(com.android.internal.R.id.icon1);
+ mIcon2 = (ImageView) v.findViewById(com.android.internal.R.id.icon2);
+ }
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ChildViewCache views = (ChildViewCache) view.getTag();
+ boolean isHtml = false;
+ if (mFormatCol >= 0) {
+ String format = cursor.getString(mFormatCol);
+ isHtml = "html".equals(format);
+ }
+ setViewText(cursor, views.mText1, mText1Col, isHtml);
+ setViewText(cursor, views.mText2, mText2Col, isHtml);
+ setViewIcon(cursor, views.mIcon1, mIconBitmap1Col, mIconName1Col);
+ setViewIcon(cursor, views.mIcon2, mIconBitmap2Col, mIconName2Col);
+ }
+
+ private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) {
+ if (v == null) {
+ return;
+ }
+ CharSequence text = null;
+ if (textCol >= 0) {
+ String str = cursor.getString(textCol);
+ text = (str != null && isHtml) ? Html.fromHtml(str) : str;
+ }
+ // Set the text even if it's null, since we need to clear any previous text.
+ v.setText(text);
+
+ if (TextUtils.isEmpty(text)) {
+ v.setVisibility(View.GONE);
+ } else {
+ v.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void setViewIcon(Cursor cursor, ImageView v, int iconBitmapCol, int iconNameCol) {
+ if (v == null) {
+ return;
+ }
+ Drawable drawable = null;
+ // First try the bitmap column
+ if (iconBitmapCol >= 0) {
+ byte[] data = cursor.getBlob(iconBitmapCol);
+ if (data != null) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (bitmap != null) {
+ drawable = new BitmapDrawable(bitmap);
+ }
+ }
+ }
+ // If there was no bitmap, try the icon resource column.
+ if (drawable == null && iconNameCol >= 0) {
+ String value = cursor.getString(iconNameCol);
+ drawable = getDrawableFromResourceValue(value);
+ }
+ // Set the icon even if the drawable is null, since we need to clear any
+ // previous icon.
+ v.setImageDrawable(drawable);
+
+ if (drawable == null) {
+ v.setVisibility(View.GONE);
+ } else {
+ v.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Gets the text to show in the query field when a suggestion is selected.
+ *
+ * @param cursor The Cursor to read the suggestion data from. The Cursor should already
+ * be moved to the suggestion that is to be read from.
+ * @return The text to show, or null
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;
+ }
+
+ // First, check the cache.
+ Drawable drawable = mOutsideDrawablesCache.get(drawableId);
+ if (drawable != null) return drawable;
+
+ try {
+ // Not cached, try using it as a plain resource ID in the provider's context.
+ int resourceId = Integer.parseInt(drawableId);
+ drawable = mProviderContext.getResources().getDrawable(resourceId);
+ } catch (NumberFormatException nfe) {
+ // The id was not an integer resource id.
+ // Let the ContentResolver handle content, android.resource and file URIs.
+ try {
+ Uri uri = Uri.parse(drawableId);
+ drawable = Drawable.createFromStream(
+ mProviderContext.getContentResolver().openInputStream(uri),
+ null);
+ } catch (FileNotFoundException fnfe) {
+ // drawable = null;
+ }
+
+ // If we got a drawable for this resource id, then stick it in the
+ // map so we don't do this lookup again.
+ if (drawable != null) {
+ mOutsideDrawablesCache.put(drawableId, drawable);
+ }
+ } catch (NotFoundException nfe) {
+ // Resource could not be found
+ // drawable = null;
+ }
+
+ return drawable;
+ }
+
+ /**
+ * Gets the value of a string column by name.
+ *
+ * @param cursor Cursor to read the value from.
+ * @param columnName The name of the column to read.
+ * @return The value of the given column, or null
+ * if the cursor does not contain the given column.
+ */
+ public static String getColumnString(Cursor cursor, String columnName) {
+ int col = cursor.getColumnIndex(columnName);
+ if (col == -1) {
+ return null;
+ }
+ return cursor.getString(col);
+ }
+
+}
diff --git a/core/java/android/appwidget/AppWidgetProvider.java b/core/java/android/appwidget/AppWidgetProvider.java
index f70de9cde67e09bdf339327008168381ecd8296f..26712a10f0ce45fe4c34dc75c04b85a7ab696f5c 100755
--- a/core/java/android/appwidget/AppWidgetProvider.java
+++ b/core/java/android/appwidget/AppWidgetProvider.java
@@ -22,7 +22,7 @@ import android.content.Intent;
import android.os.Bundle;
/**
- * A conveience class to aid in implementing an AppWidget provider.
+ * A convenience class to aid in implementing an AppWidget provider.
* Everything you can do with AppWidgetProvider, you can do with a regular {@link BroadcastReceiver}.
* AppWidgetProvider merely parses the relevant fields out of the Intent that is received in
* {@link #onReceive(Context,Intent) onReceive(Context,Intent)}, and calls hook methods
@@ -30,11 +30,9 @@ import android.os.Bundle;
*
* Extend this class and override one or more of the {@link #onUpdate}, {@link #onDeleted},
* {@link #onEnabled} or {@link #onDisabled} methods to implement your own AppWidget functionality.
- *
- *
Sample Code
- * For an example of how to write a AppWidget provider, see the
- * android.appwidget
- * package overview.
+ *
+ * For an example of how to write a AppWidget provider, see the
+ * AppWidgets documentation.
*/
public class AppWidgetProvider extends BroadcastReceiver {
/**
diff --git a/core/java/android/appwidget/package.html b/core/java/android/appwidget/package.html
index b6cd9c74d02ef04c51cc744b943344b9bab09b59..2b85bd5a41a4e9abed7da506fb563e5672a6dae6 100644
--- a/core/java/android/appwidget/package.html
+++ b/core/java/android/appwidget/package.html
@@ -3,127 +3,22 @@
views are called widgets, and are published by "AppWidget providers." The component that can
contain widgets is called a "AppWidget host."
-AppWidget Providers
-
- - Declaring a widget in the AndroidManifest
- - Adding the AppWidgetProviderInfo meta-data
- - Using the AppWidgetProvider class
- - AppWidget Configuration UI
- - AppWidget Broadcast Intents
-
-AppWidget Hosts
+For more information, see the
+AppWidgets
+documentation in the Dev Guide.
{@more}
AppWidget Providers
-
-Any application can publish widgets. All an application needs to do to publish a widget is
+
Any application can publish widgets. All an application needs to do to publish a widget is
to have a {@link android.content.BroadcastReceiver} that receives the {@link
android.appwidget.AppWidgetManager#ACTION_APPWIDGET_UPDATE AppWidgetManager.ACTION_APPWIDGET_UPDATE} intent,
and provide some meta-data about the widget. Android provides the
{@link android.appwidget.AppWidgetProvider} class, which extends BroadcastReceiver, as a convenience
class to aid in handling the broadcasts.
-
Declaring a widget in the AndroidManifest
-
-
-First, declare the {@link android.content.BroadcastReceiver} in your application's
-AndroidManifest.xml
file.
-
-{@sample frameworks/base/tests/appwidgets/AppWidgetHostTest/AndroidManifest.xml AppWidgetProvider}
-
-
-The <receiver>
element has the following attributes:
-
- android:name
- which specifies the
- {@link android.content.BroadcastReceiver} or {@link android.appwidget.AppWidgetProvider}
- class.
- android:label
- which specifies the string resource that
- will be shown by the widget picker as the label.
- android:icon
- which specifies the drawable resource that
- will be shown by the widget picker as the icon.
-
-
-
-The <intent-filter>
element tells the {@link android.content.pm.PackageManager}
-that this {@link android.content.BroadcastReceiver} receives the {@link
-android.appwidget.AppWidgetManager#ACTION_APPWIDGET_UPDATE AppWidgetManager.ACTION_APPWIDGET_UPDATE} broadcast.
-The widget manager will send other broadcasts directly to your widget provider as required.
-It is only necessary to explicitly declare that you accept the {@link
-android.appwidget.AppWidgetManager#ACTION_APPWIDGET_UPDATE AppWidgetManager.ACTION_APPWIDGET_UPDATE} broadcast.
-
-
-The <meta-data>
element tells the widget manager which xml resource to
-read to find the {@link android.appwidget.AppWidgetProviderInfo} for your widget provider. It has the following
-attributes:
-
- android:name="android.appwidget.provider"
- identifies this meta-data
- as the {@link android.appwidget.AppWidgetProviderInfo} descriptor.
- android:resource
- is the xml resource to use as that descriptor.
-
-
-
-Adding the {@link android.appwidget.AppWidgetProviderInfo AppWidgetProviderInfo} meta-data
-
-
-For a widget, the values in the {@link android.appwidget.AppWidgetProviderInfo} structure are supplied
-in an XML resource. In the example above, the xml resource is referenced with
-android:resource="@xml/appwidget_info"
. That XML file would go in your application's
-directory at res/xml/appwidget_info.xml
. Here is a simple example.
-
-{@sample frameworks/base/tests/appwidgets/AppWidgetHostTest/res/xml/appwidget_info.xml AppWidgetProviderInfo}
-
-
-The attributes are as documented in the {@link android.appwidget.AppWidgetProviderInfo GagetInfo} class. (86400000 milliseconds means once per day)
-
-
-
Using the {@link android.appwidget.AppWidgetProvider AppWidgetProvider} class
-
-The AppWidgetProvider class is the easiest way to handle the widget provider intent broadcasts.
-See the src/com/example/android/apis/appwidget/ExampleAppWidgetProvider.java
-sample class in ApiDemos for an example.
-
-
Keep in mind that since the the AppWidgetProvider is a BroadcastReceiver,
-your process is not guaranteed to keep running after the callback methods return. See
-Application Fundamentals >
-Broadcast Receiver Lifecycle for more information.
-
-
-
-
AppWidget Configuration UI
-
-
-Widget hosts have the ability to start a configuration activity when a widget is instantiated.
-The activity should be declared as normal in AndroidManifest.xml, and it should be listed in
-the AppWidgetProviderInfo XML file in the android:configure
attribute.
-
-
The activity you specified will be launched with the {@link
-android.appwidget.AppWidgetManager#ACTION_APPWIDGET_CONFIGURE} action. See the documentation for that
-action for more info.
-
-
See the src/com/example/android/apis/appwidget/ExampleAppWidgetConfigure.java
-sample class in ApiDemos for an example.
-
-
-
-
AppWidget Broadcast Intents
-
-{@link android.appwidget.AppWidgetProvider} is just a convenience class. If you would like
-to receive the widget broadcasts directly, you can. The four intents you need to care about are:
-
- - {@link android.appwidget.AppWidgetManager#ACTION_APPWIDGET_UPDATE}
- - {@link android.appwidget.AppWidgetManager#ACTION_APPWIDGET_DELETED}
- - {@link android.appwidget.AppWidgetManager#ACTION_APPWIDGET_ENABLED}
- - {@link android.appwidget.AppWidgetManager#ACTION_APPWIDGET_DISABLED}
-
-
-By way of example, the implementation of
-{@link android.appwidget.AppWidgetProvider#onReceive} is quite simple:
-
-{@sample frameworks/base/core/java/android/appwidget/AppWidgetProvider.java onReceive}
-
AppWidget Hosts
Widget hosts are the containers in which widgets can be placed. Most of the look and feel
@@ -132,5 +27,6 @@ widgets, but the lock screen could also contain widgets, and it would have a dif
adding, removing and otherwise managing widgets.
For more information on implementing your own widget host, see the
{@link android.appwidget.AppWidgetHost AppWidgetHost} class.
+