diff options
Diffstat (limited to 'core/java/android/app/SearchDialog.java')
-rw-r--r-- | core/java/android/app/SearchDialog.java | 243 |
1 files changed, 221 insertions, 22 deletions
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 27c6376..18e4a52 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -34,7 +34,10 @@ import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; import android.os.SystemClock; +import android.provider.Browser; import android.server.search.SearchableInfo; import android.speech.RecognizerIntent; import android.text.Editable; @@ -42,11 +45,13 @@ import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.text.util.Regex; +import android.util.AndroidRuntimeException; import android.util.AttributeSet; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.KeyEvent; +import android.view.Menu; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; @@ -240,7 +245,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } return success; } - + + private boolean isInRealAppSearch() { + return !mGlobalSearchMode + && (mPreviousComponents == null || mPreviousComponents.isEmpty()); + } + /** * Called in response to a press of the hard search button in * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app @@ -260,6 +270,16 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (!mGlobalSearchMode) { mStoredComponentName = mLaunchComponent; mStoredAppSearchData = mAppSearchData; + + // If this is the browser, we have a special case to not show the icon to the left + // of the text field, for extra space for url entry (this should be reconciled in + // Eclair). So special case a second tap of the search button to remove any + // already-entered text so that we can be sure to show the "Quick Search Box" hint + // text to still make it clear to the user that we've jumped out to global search. + // + // TODO: When the browser icon issue is reconciled in Eclair, remove this special case. + if (isBrowserSearch()) currentSearchText = ""; + return doShow(currentSearchText, false, null, mAppSearchData, true); } else { if (mStoredComponentName != null) { @@ -531,12 +551,14 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // we dismiss the entire dialog instead mSearchAutoComplete.setDropDownDismissedOnCompletion(false); - if (mGlobalSearchMode) { + if (!isInRealAppSearch()) { mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in } else { mSearchAutoComplete.setDropDownAlwaysVisible(false); } + mSearchAutoComplete.setForceIgnoreOutsideTouch(true); + // attach the suggestions adapter, if suggestions are available // The existence of a suggestions authority is the proxy for "suggestions available here" if (mSearchable.getSuggestAuthority() != null) { @@ -565,7 +587,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } private void updateSearchAppIcon() { - if (mGlobalSearchMode) { + // In Donut, we special-case the case of the browser to hide the app icon as if it were + // global search, for extra space for url entry. + // + // TODO: Remove this special case once the issue has been reconciled in Eclair. + if (mGlobalSearchMode || isBrowserSearch()) { mAppIcon.setImageResource(0); mAppIcon.setVisibility(View.GONE); mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL, @@ -658,6 +684,49 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** + * Hack to determine whether this is the browser, so we can remove the browser icon + * to the left of the search field, as a special requirement for Donut. + * + * TODO: For Eclair, reconcile this with the rest of the global search UI. + */ + private boolean isBrowserSearch() { + return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/"); + } + + /* + * Menu. + */ + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Show search settings menu item if anyone handles the intent for it + Intent settingsIntent = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS); + settingsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PackageManager pm = getContext().getPackageManager(); + ActivityInfo activityInfo = settingsIntent.resolveActivityInfo(pm, 0); + if (activityInfo != null) { + settingsIntent.setClassName(activityInfo.applicationInfo.packageName, + activityInfo.name); + CharSequence label = activityInfo.loadLabel(getContext().getPackageManager()); + menu.add(Menu.NONE, Menu.NONE, Menu.NONE, label) + .setIcon(android.R.drawable.ic_menu_preferences) + .setAlphabeticShortcut('P') + .setIntent(settingsIntent); + return true; + } + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onMenuOpened(int featureId, Menu menu) { + // The menu shows up above the IME, regardless of whether it is in front + // of the drop-down or not. This looks weird when there is no IME, so + // we make sure it is visible. + mSearchAutoComplete.ensureImeVisible(); + return super.onMenuOpened(featureId, menu); + } + + /** * Listeners of various types */ @@ -794,7 +863,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS 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)) { @@ -835,6 +903,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS getContext().startActivity(mVoiceWebSearchIntent); } else if (mSearchable.getVoiceSearchLaunchRecognizer()) { Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent); + + // Stop the existing search before starting voice search, or else we'll end + // up showing the search dialog again once we return to the app. + ((SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE)). + stopSearch(); + getContext().startActivity(appSearchIntent); } } catch (ActivityNotFoundException e) { @@ -1093,7 +1167,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS */ protected void launchQuerySearch(int actionKey, String actionMsg) { String query = mSearchAutoComplete.getText().toString(); - Intent intent = createIntent(Intent.ACTION_SEARCH, null, null, query, null, + String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH; + Intent intent = createIntent(action, null, null, query, null, actionKey, actionMsg); launchIntent(intent); } @@ -1169,11 +1244,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // ensure the icons will work for global search cv.put(SearchManager.SUGGEST_COLUMN_ICON_1, wrapIconForPackage( - source, + mSearchable.getSuggestPackage(), getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1))); cv.put(SearchManager.SUGGEST_COLUMN_ICON_2, wrapIconForPackage( - source, + mSearchable.getSuggestPackage(), getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2))); // the rest can be passed through directly @@ -1212,11 +1287,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * Wraps an icon for a particular package. If the icon is a resource id, it is converted into * an android.resource:// URI. * - * @param source The source of the icon + * @param packageName The source of the icon * @param icon The icon retrieved from a suggestion column * @return An icon string appropriate for the package. */ - private String wrapIconForPackage(ComponentName source, String icon) { + private String wrapIconForPackage(String packageName, String icon) { if (icon == null || icon.length() == 0 || "0".equals(icon)) { // SearchManager specifies that null or zero can be returned to indicate // no icon. We also allow empty string. @@ -1224,7 +1299,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } else if (!Character.isDigit(icon.charAt(0))){ return icon; } else { - String packageName = source.getPackageName(); return new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(packageName) @@ -1245,16 +1319,133 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return; } Log.d(LOG_TAG, "launching " + intent); - getContext().startActivity(intent); + try { + // in global search mode, we send the activity straight to the original suggestion + // source. this is because GlobalSearch may not have permission to launch the + // intent, and to avoid the extra step of going through GlobalSearch. + if (mGlobalSearchMode) { + launchGlobalSearchIntent(intent); + } else { + // If the intent was created from a suggestion, it will always have an explicit + // component here. + Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI()); + getContext().startActivity(intent); + // If the search switches to a different activity, + // SearchDialogWrapper#performActivityResuming + // will handle hiding the dialog when the next activity starts, but for + // real in-app search, we still need to dismiss the dialog. + if (isInRealAppSearch()) { + dismiss(); + } + } + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); + } + } - // in global search mode, SearchDialogWrapper#performActivityResuming will handle hiding - // the dialog when the next activity starts, but for in-app search, we still need to - // dismiss the dialog. - if (!mGlobalSearchMode) { - dismiss(); + private void launchGlobalSearchIntent(Intent intent) { + final String packageName; + // GlobalSearch puts the original source of the suggestion in the + // 'component name' column. If set, we send the intent to that activity. + // We trust GlobalSearch to always set this to the suggestion source. + String intentComponent = intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY); + if (intentComponent != null) { + ComponentName componentName = ComponentName.unflattenFromString(intentComponent); + intent.setComponent(componentName); + intent.removeExtra(SearchManager.COMPONENT_NAME_KEY); + // Launch the intent as the suggestion source. + // This prevents sources from using the search dialog to launch + // intents that they don't have permission for themselves. + packageName = componentName.getPackageName(); + } else { + // If there is no component in the suggestion, it must be a built-in suggestion + // from GlobalSearch (e.g. "Search the web for") or the intent + // launched when pressing the search/go button in the search dialog. + // Launch the intent with the permissions of GlobalSearch. + packageName = mSearchable.getSearchActivity().getPackageName(); } + + // Launch all global search suggestions as new tasks, since they don't relate + // to the current task. + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + setBrowserApplicationId(intent); + + startActivityInPackage(intent, packageName); } - + + /** + * If the intent is to open an HTTP or HTTPS URL, we set + * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that + * has been opened by us for the same URL will be reused. + */ + private void setBrowserApplicationId(Intent intent) { + Uri data = intent.getData(); + if (Intent.ACTION_VIEW.equals(intent.getAction()) && data != null) { + String scheme = data.getScheme(); + if (scheme != null && scheme.startsWith("http")) { + intent.putExtra(Browser.EXTRA_APPLICATION_ID, data.toString()); + } + } + } + + /** + * Starts an activity as if it had been started by the given package. + * + * @param intent The description of the activity to start. + * @param packageName + * @throws ActivityNotFoundException If the intent could not be resolved to + * and existing activity. + * @throws SecurityException If the package does not have permission to start + * start the activity. + * @throws AndroidRuntimeException If some other error occurs. + */ + private void startActivityInPackage(Intent intent, String packageName) { + try { + int uid = ActivityThread.getPackageManager().getPackageUid(packageName); + if (uid < 0) { + throw new AndroidRuntimeException("Package UID not found " + packageName); + } + String resolvedType = intent.resolveTypeIfNeeded(getContext().getContentResolver()); + IBinder resultTo = null; + String resultWho = null; + int requestCode = -1; + boolean onlyIfNeeded = false; + Log.i(LOG_TAG, "Starting (uid " + uid + ", " + packageName + ") " + intent.toURI()); + int result = ActivityManagerNative.getDefault().startActivityInPackage( + uid, intent, resolvedType, resultTo, resultWho, requestCode, onlyIfNeeded); + checkStartActivityResult(result, intent); + } catch (RemoteException ex) { + throw new AndroidRuntimeException(ex); + } + } + + // Stolen from Instrumentation.checkStartActivityResult() + private static void checkStartActivityResult(int res, Intent intent) { + if (res >= IActivityManager.START_SUCCESS) { + return; + } + switch (res) { + case IActivityManager.START_INTENT_NOT_RESOLVED: + case IActivityManager.START_CLASS_NOT_FOUND: + if (intent.getComponent() != null) + throw new ActivityNotFoundException( + "Unable to find explicit activity class " + + intent.getComponent().toShortString() + + "; have you declared this activity in your AndroidManifest.xml?"); + throw new ActivityNotFoundException( + "No Activity found to handle " + intent); + case IActivityManager.START_PERMISSION_DENIED: + throw new SecurityException("Not allowed to start activity " + + intent); + case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: + throw new AndroidRuntimeException( + "FORWARD_RESULT_FLAG used while also requesting a result"); + default: + throw new AndroidRuntimeException("Unknown error code " + + res + " when starting " + intent); + } + } + /** * Handles the special intent actions declared in {@link SearchManager}. * @@ -1284,13 +1475,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return; } if (DBG) Log.d(LOG_TAG, "Switching to " + componentName); - - ComponentName previous = mLaunchComponent; + + pushPreviousComponent(mLaunchComponent); if (!show(componentName, mAppSearchData, false)) { Log.w(LOG_TAG, "Failed to switch to source " + componentName); + popPreviousComponent(); return; } - pushPreviousComponent(previous); String query = intent.getStringExtra(SearchManager.QUERY); setUserQuery(query); @@ -1460,8 +1651,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS 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()); + // Only allow 3rd-party intents from GlobalSearch + if (!mGlobalSearchMode) { + intent.setComponent(mSearchable.getSearchActivity()); + } return intent; } @@ -1582,6 +1775,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (mSearchDialog.backToPreviousComponent()) { return true; } + // If the drop-down obscures the keyboard, the user wouldn't see anything + // happening when pressing back, so we dismiss the entire dialog instead. + if (isInputMethodNotNeeded()) { + mSearchDialog.cancel(); + return true; + } return false; // will dismiss soft keyboard if necessary } return false; |