summaryrefslogtreecommitdiffstats
path: root/core/java/android/app/SearchDialog.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/app/SearchDialog.java')
-rw-r--r--core/java/android/app/SearchDialog.java243
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;