/* * 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.xbrowser; import com.android.xbrowser.Controller; import com.android.xbrowser.R; import com.android.xbrowser.UI.DropdownChangeListener; import com.android.xbrowser.provider.BrowserProvider; import com.android.xbrowser.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.getApplicationContext(); 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(null); } private final class BrowserSearchboxListener extends 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 WebView current = getCurrentWebview(); if (current == null) { return; } final SearchBox searchBox = current.getSearchBox(); if (searchBox != mSearchBox) { if (mSearchBox != null) { mSearchBox.removeSearchBoxListener(mListener); mListener.clear(); } mSearchBox = searchBox; if (mSearchBox != null) { mSearchBox.addSearchBoxListener(mListener); } } } private boolean isInstantPage() { final WebView current = getCurrentWebview(); if (current == null) { return false; } final String currentUrl = mController.getCurrentTab().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() { final WebView current = getCurrentWebview(); if (current != null) { current.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); if (mSearchBox == null) { return mWrapped.getSuggestions(context, query); } mSearchBox.setDimensions(0, 0, 0, mHeight); mSearchBox.onresize(null); 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(null); // 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 WebView current = getCurrentWebview(); if (current == null) { return 0; } final float scale = current.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; if (mSearchBox != null) { mSearchBox.setDimensions(0, 0, 0, rescaledHeight); mSearchBox.onresize(null); } } } 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(); } } }