summaryrefslogtreecommitdiffstats
path: root/src/com/android/browser/InstantSearchEngine.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/browser/InstantSearchEngine.java')
-rw-r--r--src/com/android/browser/InstantSearchEngine.java407
1 files changed, 407 insertions, 0 deletions
diff --git a/src/com/android/browser/InstantSearchEngine.java b/src/com/android/browser/InstantSearchEngine.java
new file mode 100644
index 0000000..1d9bdd6
--- /dev/null
+++ b/src/com/android/browser/InstantSearchEngine.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.browser;
+
+import com.google.android.collect.Maps;
+import com.google.common.collect.Lists;
+
+import com.android.browser.Controller;
+import com.android.browser.R;
+import com.android.browser.UI.DropdownChangeListener;
+import com.android.browser.search.DefaultSearchEngine;
+import com.android.browser.search.SearchEngine;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+import android.webkit.SearchBox;
+import android.webkit.WebView;
+
+import java.util.Collections;
+import java.util.List;
+
+public class InstantSearchEngine implements SearchEngine, DropdownChangeListener {
+ private static final String TAG = "Browser.InstantSearchEngine";
+ private static final boolean DBG = false;
+
+ private Controller mController;
+ private SearchBox mSearchBox;
+ private final BrowserSearchboxListener mListener = new BrowserSearchboxListener();
+ private int mHeight;
+
+ private String mInstantBaseUrl;
+ private final Context mContext;
+ // Used for startSearch( ) calls if for some reason instant
+ // is off, or no searchbox is present.
+ private final SearchEngine mWrapped;
+
+ public InstantSearchEngine(Context context, SearchEngine wrapped) {
+ mContext = context;
+ mWrapped = wrapped;
+ }
+
+ public void setController(Controller controller) {
+ mController = controller;
+ }
+
+ @Override
+ public String getName() {
+ return SearchEngine.GOOGLE;
+ }
+
+ @Override
+ public CharSequence getLabel() {
+ return mContext.getResources().getString(R.string.instant_search_label);
+ }
+
+ @Override
+ public void startSearch(Context context, String query, Bundle appData, String extraData) {
+ if (DBG) Log.d(TAG, "startSearch(" + query + ")");
+
+ switchSearchboxIfNeeded();
+
+ // If for some reason we are in a bad state, ensure that the
+ // user gets default search results at the very least.
+ if (mSearchBox == null & !isInstantPage()) {
+ mWrapped.startSearch(context, query, appData, extraData);
+ return;
+ }
+
+ mSearchBox.setQuery(query);
+ mSearchBox.setVerbatim(true);
+ mSearchBox.onsubmit();
+ }
+
+ private final class BrowserSearchboxListener implements SearchBox.SearchBoxListener {
+ /*
+ * The maximum number of out of order suggestions we accept
+ * before giving up the wait.
+ */
+ private static final int MAX_OUT_OF_ORDER = 5;
+
+ /*
+ * We wait for suggestions in increments of 600ms. This is primarily to
+ * guard against suggestions arriving out of order.
+ */
+ private static final int WAIT_INCREMENT_MS = 600;
+
+ /*
+ * A cache of suggestions received, keyed by the queries they were
+ * received for.
+ */
+ private final LruCache<String, List<String>> mSuggestions =
+ new LruCache<String, List<String>>(20);
+
+ /*
+ * The last set of suggestions received. We use this reduce UI flicker
+ * in case there is a delay in recieving suggestions.
+ */
+ private List<String> mLatestSuggestion = Collections.emptyList();
+
+ @Override
+ public synchronized void onSuggestionsReceived(String query, List<String> suggestions) {
+ if (DBG) Log.d(TAG, "onSuggestionsReceived(" + query + ")");
+
+ if (!TextUtils.isEmpty(query)) {
+ mSuggestions.put(query, suggestions);
+ mLatestSuggestion = suggestions;
+ }
+
+ notifyAll();
+ }
+
+ public synchronized List<String> tryWaitForSuggestions(String query) {
+ if (DBG) Log.d(TAG, "tryWait(" + query + ")");
+
+ int numWaitReturns = 0;
+
+ // This slightly unusual waiting construct is used to safeguard
+ // to some extent against suggestions arriving out of order. We
+ // wait for upto 5 notifyAll( ) calls to check if we received
+ // suggestions for a given query.
+ while (mSuggestions.get(query) == null) {
+ try {
+ wait(WAIT_INCREMENT_MS);
+ ++numWaitReturns;
+ if (numWaitReturns > MAX_OUT_OF_ORDER) {
+ // We've waited too long for suggestions to be returned.
+ // return the last available suggestion.
+ break;
+ }
+ } catch (InterruptedException e) {
+ return Collections.emptyList();
+ }
+ }
+
+ List<String> suggestions = mSuggestions.get(query);
+ if (suggestions == null) {
+ return mLatestSuggestion;
+ }
+
+ return suggestions;
+ }
+
+ public synchronized void clear() {
+ mSuggestions.evictAll();
+ }
+ }
+
+ private WebView getCurrentWebview() {
+ if (mController != null) {
+ return mController.getTabControl().getCurrentTopWebView();
+ }
+
+ return null;
+ }
+
+ /**
+ * Attaches the searchbox to the right browser page, i.e, the currently
+ * visible tab.
+ */
+ private void switchSearchboxIfNeeded() {
+ final SearchBox searchBox = getCurrentWebview().getSearchBox();
+ if (searchBox != mSearchBox) {
+ if (mSearchBox != null) {
+ mSearchBox.removeSearchBoxListener(mListener);
+ mListener.clear();
+ }
+ mSearchBox = searchBox;
+ mSearchBox.addSearchBoxListener(mListener);
+ }
+ }
+
+ private boolean isInstantPage() {
+ String currentUrl = getCurrentWebview().getUrl();
+
+ if (currentUrl != null) {
+ Uri uri = Uri.parse(currentUrl);
+ final String host = uri.getHost();
+ final String path = uri.getPath();
+
+ // Is there a utility class that does this ?
+ if (path != null && host != null) {
+ return host.startsWith("www.google.") &&
+ (path.startsWith("/search") || path.startsWith("/webhp"));
+ }
+ return false;
+ }
+
+ return false;
+ }
+
+ private void loadInstantPage() {
+ mController.getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ getCurrentWebview().loadUrl(getInstantBaseUrl());
+ }
+ });
+ }
+
+ /**
+ * Queries for a given search term and returns a cursor containing
+ * suggestions ordered by best match.
+ */
+ @Override
+ public Cursor getSuggestions(Context context, String query) {
+ if (DBG) Log.d(TAG, "getSuggestions(" + query + ")");
+ if (query == null) {
+ return null;
+ }
+
+ if (!isInstantPage()) {
+ loadInstantPage();
+ }
+
+ switchSearchboxIfNeeded();
+
+ mController.registerDropdownChangeListener(this);
+
+ mSearchBox.setDimensions(0, 0, 0, mHeight);
+ mSearchBox.onresize();
+
+ if (TextUtils.isEmpty(query)) {
+ // To force the SRP to render an empty (no results) page.
+ mSearchBox.setVerbatim(true);
+ } else {
+ mSearchBox.setVerbatim(false);
+ }
+ mSearchBox.setQuery(query);
+ mSearchBox.onchange();
+
+ // Don't bother waiting for suggestions for an empty query. We still
+ // set the query so that the SRP clears itself.
+ if (TextUtils.isEmpty(query)) {
+ return new SuggestionsCursor(Collections.<String>emptyList());
+ } else {
+ return new SuggestionsCursor(mListener.tryWaitForSuggestions(query));
+ }
+ }
+
+ @Override
+ public boolean supportsSuggestions() {
+ return true;
+ }
+
+ @Override
+ public void close() {
+ if (mController != null) {
+ mController.registerDropdownChangeListener(null);
+ }
+ if (mSearchBox != null) {
+ mSearchBox.removeSearchBoxListener(mListener);
+ }
+ mListener.clear();
+ mWrapped.close();
+ }
+
+ @Override
+ public boolean supportsVoiceSearch() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "InstantSearchEngine {" + hashCode() + "}";
+ }
+
+ @Override
+ public boolean wantsEmptyQuery() {
+ return true;
+ }
+
+ private int rescaleHeight(int height) {
+ final float scale = getCurrentWebview().getScale();
+ if (scale != 0) {
+ return (int) (height / scale);
+ }
+
+ return height;
+ }
+
+ @Override
+ public void onNewDropdownDimensions(int height) {
+ final int rescaledHeight = rescaleHeight(height);
+
+ if (rescaledHeight != mHeight) {
+ mHeight = rescaledHeight;
+ mSearchBox.setDimensions(0, 0, 0, rescaledHeight);
+ mSearchBox.onresize();
+ }
+ }
+
+ private String getInstantBaseUrl() {
+ if (mInstantBaseUrl == null) {
+ String url = mContext.getResources().getString(R.string.instant_base);
+ if (url.indexOf("{CID}") != -1) {
+ url = url.replace("{CID}",
+ BrowserProvider.getClientId(mContext.getContentResolver()));
+ }
+ mInstantBaseUrl = url;
+ }
+
+ return mInstantBaseUrl;
+ }
+
+ // Indices of the columns in the below arrays.
+ private static final int COLUMN_INDEX_ID = 0;
+ private static final int COLUMN_INDEX_QUERY = 1;
+ private static final int COLUMN_INDEX_ICON = 2;
+ private static final int COLUMN_INDEX_TEXT_1 = 3;
+
+ private static final String[] COLUMNS_WITHOUT_DESCRIPTION = new String[] {
+ "_id",
+ SearchManager.SUGGEST_COLUMN_QUERY,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ };
+
+ private static class SuggestionsCursor extends AbstractCursor {
+ private final List<String> mSuggestions;
+
+ public SuggestionsCursor(List<String> suggestions) {
+ mSuggestions = suggestions;
+ }
+
+ @Override
+ public int getCount() {
+ return mSuggestions.size();
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return COLUMNS_WITHOUT_DESCRIPTION;
+ }
+
+ private String format(String suggestion) {
+ if (TextUtils.isEmpty(suggestion)) {
+ return "";
+ }
+ return suggestion;
+ }
+
+ @Override
+ public String getString(int column) {
+ if (mPos >= 0 && mPos < mSuggestions.size()) {
+ if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) {
+ return format(mSuggestions.get(mPos));
+ } else if (column == COLUMN_INDEX_ICON) {
+ return String.valueOf(R.drawable.magnifying_glass);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public double getDouble(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public float getFloat(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getInt(int column) {
+ if (column == COLUMN_INDEX_ID) {
+ return mPos;
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getLong(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public short getShort(int column) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}