diff options
Diffstat (limited to 'src/com/android/browser/InstantSearchEngine.java')
-rw-r--r-- | src/com/android/browser/InstantSearchEngine.java | 407 |
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(); + } + } +} |