diff options
author | Bjorn Bringert <bringert@android.com> | 2011-03-03 00:12:50 -0800 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2011-03-03 00:12:50 -0800 |
commit | 02f97585a675aba7ae81554060bbf2d526b76c61 (patch) | |
tree | aa37d6a3eb88e6ac6b025f490e912aca9f4a1b20 /src/com/android | |
parent | 57996ae649fbfc157ec360f86d154e0c3b57b9ad (diff) | |
parent | 5119edd5744cfc6d3a8ed480a8853586c737bed4 (diff) | |
download | packages_apps_Browser-02f97585a675aba7ae81554060bbf2d526b76c61.zip packages_apps_Browser-02f97585a675aba7ae81554060bbf2d526b76c61.tar.gz packages_apps_Browser-02f97585a675aba7ae81554060bbf2d526b76c61.tar.bz2 |
Merge "Implement the psychic search engine."
Diffstat (limited to 'src/com/android')
17 files changed, 690 insertions, 109 deletions
diff --git a/src/com/android/browser/BaseUi.java b/src/com/android/browser/BaseUi.java index 93b0ec8..c01ec06 100644 --- a/src/com/android/browser/BaseUi.java +++ b/src/com/android/browser/BaseUi.java @@ -17,6 +17,7 @@ package com.android.browser; import com.android.browser.Tab.LockIcon; +import com.android.browser.UI.DropdownChangeListener; import android.animation.Animator; import android.animation.Animator.AnimatorListener; @@ -698,4 +699,7 @@ public abstract class BaseUi implements UI, WebViewFactory { warning.show(); } + @Override + public void registerDropdownChangeListener(DropdownChangeListener d) { + } } diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java index f3bc48a..75e0cfb 100644 --- a/src/com/android/browser/BrowserSettings.java +++ b/src/com/android/browser/BrowserSettings.java @@ -113,6 +113,7 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha // Lab settings private boolean quickControls = false; private boolean useMostVisitedHomepage = false; + private boolean useInstant = false; // By default the error console is shown once the user navigates to about:debug. // The setting can be then toggled from the settings menu. @@ -170,6 +171,7 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha public final static String PREF_AUTOLOGIN = "enable_autologin"; public final static String PREF_AUTOLOGIN_ACCOUNT = "autologin_account"; public final static String PREF_PLUGIN_STATE = "plugin_state"; + public final static String PREF_USE_INSTANT = "use_instant_search"; private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (Macintosh; " + "U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, " + @@ -413,27 +415,37 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha } } + private void updateSearchEngine(Context ctx, String searchEngineName, boolean force) { + if (force || searchEngine == null || + !searchEngine.getName().equals(searchEngineName)) { + if (searchEngine != null) { + if (searchEngine.supportsVoiceSearch()) { + // One or more tabs could have been in voice search mode. + // Clear it, since the new SearchEngine may not support + // it, or may handle it differently. + for (int i = 0; i < mController.getTabControl().getTabCount(); i++) { + mController.getTabControl().getTab(i).revertVoiceSearchMode(); + } + } + searchEngine.close(); + } + searchEngine = SearchEngines.get(ctx, searchEngineName); + + if (mController != null && (searchEngine instanceof InstantSearchEngine)) { + ((InstantSearchEngine) searchEngine).setController(mController); + } + } + } + /* package */ void syncSharedPreferences(Context ctx, SharedPreferences p) { homeUrl = p.getString(PREF_HOMEPAGE, homeUrl); + + useInstant = p.getBoolean(PREF_USE_INSTANT, useInstant); String searchEngineName = p.getString(PREF_SEARCH_ENGINE, - SearchEngine.GOOGLE); - if (searchEngine == null || !searchEngine.getName().equals(searchEngineName)) { - if (searchEngine != null) { - if (searchEngine.supportsVoiceSearch()) { - // One or more tabs could have been in voice search mode. - // Clear it, since the new SearchEngine may not support - // it, or may handle it differently. - for (int i = 0; i < mController.getTabControl().getTabCount(); i++) { - mController.getTabControl().getTab(i).revertVoiceSearchMode(); - } - } - searchEngine.close(); - } - searchEngine = SearchEngines.get(ctx, searchEngineName); - } - Log.i(TAG, "Selected search engine: " + searchEngine); + SearchEngine.GOOGLE); + updateSearchEngine(ctx, searchEngineName, false); loadsImagesAutomatically = p.getBoolean("load_images", loadsImagesAutomatically); @@ -604,6 +616,10 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha return useMostVisitedHomepage; } + public boolean useInstant() { + return useInstant; + } + public boolean showDebugSettings() { return showDebugSettings; } @@ -723,6 +739,10 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha /* package */void setController(Controller ctrl) { mController = ctrl; updateTabControlSettings(); + + if (mController != null && (searchEngine instanceof InstantSearchEngine)) { + ((InstantSearchEngine) searchEngine).setController(mController); + } } /* @@ -902,6 +922,13 @@ public class BrowserSettings extends Observable implements OnSharedPreferenceCha quickControls = p.getBoolean(PREF_QUICK_CONTROLS, quickControls); } else if (PREF_MOST_VISITED_HOMEPAGE.equals(key)) { useMostVisitedHomepage = p.getBoolean(PREF_MOST_VISITED_HOMEPAGE, useMostVisitedHomepage); + } else if (PREF_USE_INSTANT.equals(key)) { + useInstant = p.getBoolean(PREF_USE_INSTANT, useInstant); + updateSearchEngine(mController.getActivity(), SearchEngine.GOOGLE, true); + } else if (PREF_SEARCH_ENGINE.equals(key)) { + final String searchEngineName = p.getString(PREF_SEARCH_ENGINE, + SearchEngine.GOOGLE); + updateSearchEngine(mController.getActivity(), searchEngineName, false); } } } diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java index ae91f11..e4b0a0c 100644 --- a/src/com/android/browser/Controller.java +++ b/src/com/android/browser/Controller.java @@ -17,6 +17,7 @@ package com.android.browser; import com.android.browser.IntentHandler.UrlData; +import com.android.browser.UI.DropdownChangeListener; import com.android.browser.search.SearchEngine; import com.android.common.Search; @@ -2608,4 +2609,8 @@ public class Controller } } + @Override + public void registerDropdownChangeListener(DropdownChangeListener d) { + mUi.registerDropdownChangeListener(d); + } } 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(); + } + } +} diff --git a/src/com/android/browser/SuggestionsAdapter.java b/src/com/android/browser/SuggestionsAdapter.java index 3636bbf..6a9111f 100644 --- a/src/com/android/browser/SuggestionsAdapter.java +++ b/src/com/android/browser/SuggestionsAdapter.java @@ -24,6 +24,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.provider.BrowserContract; +import android.text.Html; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -44,12 +45,12 @@ import java.util.List; public class SuggestionsAdapter extends BaseAdapter implements Filterable, OnClickListener { - static final int TYPE_BOOKMARK = 0; - static final int TYPE_HISTORY = 1; - static final int TYPE_SUGGEST_URL = 2; - static final int TYPE_SEARCH = 3; - static final int TYPE_SUGGEST = 4; - static final int TYPE_VOICE_SEARCH = 5; + public static final int TYPE_BOOKMARK = 0; + public static final int TYPE_HISTORY = 1; + public static final int TYPE_SUGGEST_URL = 2; + public static final int TYPE_SEARCH = 3; + public static final int TYPE_SUGGEST = 4; + public static final int TYPE_VOICE_SEARCH = 5; private static final String[] COMBINED_PROJECTION = {BrowserContract.Combined._ID, BrowserContract.Combined.TITLE, @@ -58,16 +59,16 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, private static final String COMBINED_SELECTION = "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)"; - Context mContext; - Filter mFilter; + final Context mContext; + final Filter mFilter; SuggestionResults mMixedResults; List<SuggestItem> mSuggestResults, mFilterResults; List<CursorSource> mSources; boolean mLandscapeMode; - CompletionListener mListener; - int mLinesPortrait; - int mLinesLandscape; - Object mResultsLock = new Object(); + final CompletionListener mListener; + final int mLinesPortrait; + final int mLinesLandscape; + final Object mResultsLock = new Object(); List<String> mVoiceResults; boolean mReverseResults; boolean mIncognitoMode; @@ -87,6 +88,7 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, getInteger(R.integer.max_suggest_lines_portrait); mLinesLandscape = mContext.getResources(). getInteger(R.integer.max_suggest_lines_landscape); + mFilter = new SuggestFilter(); addSource(new CombinedCursor()); } @@ -111,13 +113,12 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, @Override public void onClick(View v) { SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag(); + if (R.id.icon2 == v.getId()) { // replace input field text with suggestion text - mListener.onSearch(item.title); + mListener.onSearch(getSuggestionUrl(item)); } else { - mListener.onSelect( - (TextUtils.isEmpty(item.url)? item.title : item.url), - item.type, item.extra); + mListener.onSelect(getSuggestionUrl(item), item.type, item.extra); } } @@ -179,7 +180,7 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, ImageView ic1 = (ImageView) view.findViewById(R.id.icon1); View ic2 = view.findViewById(R.id.icon2); View div = view.findViewById(R.id.divider); - tv1.setText(item.title); + tv1.setText(Html.fromHtml(item.title)); if (TextUtils.isEmpty(item.url)) { tv2.setVisibility(View.GONE); } else { @@ -282,11 +283,16 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, } } + private boolean shouldProcessEmptyQuery() { + final SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine(); + return searchEngine.wantsEmptyQuery(); + } + @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults res = new FilterResults(); if (mVoiceResults == null) { - if (TextUtils.isEmpty(constraint)) { + if (TextUtils.isEmpty(constraint) && !shouldProcessEmptyQuery()) { res.count = 0; res.values = null; return res; @@ -313,8 +319,7 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, } void mixResults(List<SuggestItem> results) { - int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait; - maxLines = (int) Math.ceil(maxLines / 2.0); + int maxLines = getMaxLines(); for (int i = 0; i < mSources.size(); i++) { CursorSource s = mSources.get(i); int n = Math.min(s.getCount(), maxLines); @@ -334,7 +339,12 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, notifyDataSetChanged(); } } + } + private int getMaxLines() { + int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait; + maxLines = (int) Math.ceil(maxLines / 2.0); + return maxLines; } /** @@ -366,11 +376,7 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, } int getLineCount() { - if (mLandscapeMode) { - return Math.min(mLinesLandscape, items.size()); - } else { - return Math.min(mLinesPortrait, items.size()); - } + return Math.min((mLandscapeMode ? mLinesLandscape : mLinesPortrait), items.size()); } @Override @@ -543,8 +549,8 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, if (mCursor != null) { mCursor.close(); } + SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine(); if (!TextUtils.isEmpty(constraint)) { - SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine(); if (searchEngine != null && searchEngine.supportsSuggestions()) { mCursor = searchEngine.getSuggestions(mContext, constraint.toString()); if (mCursor != null) { @@ -552,19 +558,44 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable, } } } else { + if (searchEngine.wantsEmptyQuery()) { + mCursor = searchEngine.getSuggestions(mContext, ""); + } mCursor = null; } } } + private boolean useInstant() { + return BrowserSettings.getInstance().useInstant(); + } + public void clearCache() { mFilterResults = null; mSuggestResults = null; + notifyDataSetInvalidated(); } public void setIncognitoMode(boolean incognito) { mIncognitoMode = incognito; clearCache(); } + + static String getSuggestionTitle(SuggestItem item) { + // There must be a better way to strip HTML from things. + // This method is used in multiple places. It is also more + // expensive than a standard html escaper. + return (item.title != null) ? Html.fromHtml(item.title).toString() : null; + } + + static String getSuggestionUrl(SuggestItem item) { + final String title = SuggestionsAdapter.getSuggestionTitle(item); + + if (TextUtils.isEmpty(item.url)) { + return title; + } + + return item.url; + } } diff --git a/src/com/android/browser/TitleBarXLarge.java b/src/com/android/browser/TitleBarXLarge.java index c0df47c..8a715b1 100644 --- a/src/com/android/browser/TitleBarXLarge.java +++ b/src/com/android/browser/TitleBarXLarge.java @@ -17,12 +17,15 @@ package com.android.browser; import com.android.browser.autocomplete.SuggestedTextController.TextChangeWatcher; +import com.android.browser.UI.DropdownChangeListener; import com.android.browser.search.SearchEngine; import android.app.Activity; import android.content.Context; import android.content.res.Resources; +import android.database.DataSetObserver; import android.graphics.Bitmap; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.view.KeyEvent; @@ -37,6 +40,7 @@ import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; +import java.util.ArrayList; import java.util.List; /** @@ -264,7 +268,7 @@ public class TitleBarXLarge extends TitleBarBase void setFavicon(Bitmap icon) { } private void clearOrClose() { - if (TextUtils.isEmpty(mUrlInput.getText())) { + if (TextUtils.isEmpty(mUrlInput.getUserText())) { // close mUrlInput.clearFocus(); } else { @@ -333,7 +337,7 @@ public class TitleBarXLarge extends TitleBarBase } private void updateSearchMode(boolean userEdited) { - setSearchMode(!userEdited || TextUtils.isEmpty(mUrlInput.getText())); + setSearchMode(!userEdited || TextUtils.isEmpty(mUrlInput.getUserText())); } private void setSearchMode(boolean voiceSearchEnabled) { @@ -411,4 +415,7 @@ public class TitleBarXLarge extends TitleBarBase } } + void registerDropdownChangeListener(DropdownChangeListener d) { + mUrlInput.registerDropdownChangeListener(d); + } } diff --git a/src/com/android/browser/UI.java b/src/com/android/browser/UI.java index 34dcaee..bec7034 100644 --- a/src/com/android/browser/UI.java +++ b/src/com/android/browser/UI.java @@ -122,4 +122,9 @@ public interface UI { boolean dispatchKey(int code, KeyEvent event); + + public static interface DropdownChangeListener { + void onNewDropdownDimensions(int height); + } + void registerDropdownChangeListener(DropdownChangeListener d); } diff --git a/src/com/android/browser/UiController.java b/src/com/android/browser/UiController.java index 6075d36..551d0ce 100644 --- a/src/com/android/browser/UiController.java +++ b/src/com/android/browser/UiController.java @@ -16,6 +16,8 @@ package com.android.browser; +import com.android.browser.UI.DropdownChangeListener; + import android.content.Intent; import android.webkit.WebView; @@ -84,4 +86,6 @@ public interface UiController extends BookmarksHistoryCallbacks { void registerOptionsMenuHandler(OptionsMenuHandler handler); void unregisterOptionsMenuHandler(OptionsMenuHandler handler); + + void registerDropdownChangeListener(DropdownChangeListener d); } diff --git a/src/com/android/browser/UrlInputView.java b/src/com/android/browser/UrlInputView.java index 40c1bf6..b7f2bff 100644 --- a/src/com/android/browser/UrlInputView.java +++ b/src/com/android/browser/UrlInputView.java @@ -18,6 +18,7 @@ package com.android.browser; import com.android.browser.SuggestionsAdapter.CompletionListener; import com.android.browser.SuggestionsAdapter.SuggestItem; +import com.android.browser.UI.DropdownChangeListener; import com.android.browser.autocomplete.SuggestiveAutoCompleteTextView; import com.android.browser.search.SearchEngine; import com.android.browser.search.SearchEngineInfo; @@ -25,11 +26,14 @@ import com.android.browser.search.SearchEngines; import android.content.Context; import android.content.res.Configuration; +import android.database.DataSetObserver; +import android.graphics.Rect; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Patterns; import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; @@ -58,6 +62,7 @@ public class UrlInputView extends SuggestiveAutoCompleteTextView private boolean mLandscape; private boolean mIncognitoMode; private boolean mNeedsUpdate; + private DropdownChangeListener mDropdownListener; public UrlInputView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); @@ -84,6 +89,22 @@ public class UrlInputView extends SuggestiveAutoCompleteTextView setThreshold(1); setOnItemClickListener(this); mNeedsUpdate = false; + mDropdownListener = null; + + mAdapter.registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + if (!isPopupShowing()) { + return; + } + dispatchChange(); + } + + @Override + public void onInvalidated() { + dispatchChange(); + } + }); } /** @@ -130,7 +151,7 @@ public class UrlInputView extends SuggestiveAutoCompleteTextView mAdapter.setLandscapeMode(mLandscape); if (isPopupShowing() && (getVisibility() == View.VISIBLE)) { setupDropDown(); - performFiltering(getText(), 0); + performFiltering(getUserText(), 0); } } @@ -158,12 +179,21 @@ public class UrlInputView extends SuggestiveAutoCompleteTextView @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - finishInput(getText().toString(), null, TYPED); + if (BrowserSettings.getInstance().useInstant() && + (actionId == EditorInfo.IME_ACTION_NEXT)) { + // When instant is turned on AND the user chooses to complete + // using the tab key, then use the completion rather than the + // text that the user has typed. + finishInput(getText().toString(), null, TYPED); + } else { + finishInput(getUserText(), null, TYPED); + } + return true; } void forceFilter() { - performFiltering(getText().toString(), 0); + performForcedFiltering(); showDropDown(); } @@ -227,8 +257,7 @@ public class UrlInputView extends SuggestiveAutoCompleteTextView public void onItemClick( AdapterView<?> parent, View view, int position, long id) { SuggestItem item = mAdapter.getItem(position); - onSelect((TextUtils.isEmpty(item.url) ? item.title : item.url), - item.type, item.extra); + onSelect(SuggestionsAdapter.getSuggestionUrl(item), item.type, item.extra); } interface UrlInputListener { @@ -258,4 +287,17 @@ public class UrlInputView extends SuggestiveAutoCompleteTextView public SuggestionsAdapter getAdapter() { return mAdapter; } + + private void dispatchChange() { + final Rect popupRect = new Rect(); + getPopupDrawableRect(popupRect); + + if (mDropdownListener != null) { + mDropdownListener.onNewDropdownDimensions(popupRect.height()); + } + } + + void registerDropdownChangeListener(DropdownChangeListener d) { + mDropdownListener = d; + } } diff --git a/src/com/android/browser/XLargeUi.java b/src/com/android/browser/XLargeUi.java index d3f83f9..33151f7 100644 --- a/src/com/android/browser/XLargeUi.java +++ b/src/com/android/browser/XLargeUi.java @@ -17,6 +17,7 @@ package com.android.browser; import com.android.browser.ScrollWebView.ScrollListener; +import com.android.browser.UI.DropdownChangeListener; import android.animation.Animator; import android.animation.Animator.AnimatorListener; @@ -483,4 +484,8 @@ public class XLargeUi extends BaseUi implements ScrollListener { return mTabBar; } + @Override + public void registerDropdownChangeListener(DropdownChangeListener d) { + mTitleBar.registerDropdownChangeListener(d); + } } diff --git a/src/com/android/browser/autocomplete/SuggestedTextController.java b/src/com/android/browser/autocomplete/SuggestedTextController.java index e9b74ce..95dfcab 100644 --- a/src/com/android/browser/autocomplete/SuggestedTextController.java +++ b/src/com/android/browser/autocomplete/SuggestedTextController.java @@ -57,6 +57,8 @@ public class SuggestedTextController { private final SuggestedSpan mSuggested; private String mSuggestedText; private TextChangeAttributes mCurrentTextChange; + private boolean mSuspended = false; + /** * While this is non-null, any changes made to the cursor position or selection are ignored. Is * stored the selection state at the moment when selection change processing was disabled. @@ -120,6 +122,10 @@ public class SuggestedTextController { } } + public boolean isCursorHandlingSuspended() { + return mSuspended; + } + public Parcelable saveInstanceState(Parcelable superState) { assertNotIgnoringSelectionChanges(); SavedState ss = new SavedState(superState); @@ -160,6 +166,7 @@ public class SuggestedTextController { assertNotIgnoringSelectionChanges(); Editable buffer = mTextOwner.getText(); mTextSelectionBeforeIgnoringChanges = new BufferSelection(buffer); + mSuspended = true; } /** @@ -181,6 +188,7 @@ public class SuggestedTextController { oldSelection.mEnd, oldSelection.mEnd, newSelection.mEnd, newSelection.mEnd); } + mSuspended = false; } /** diff --git a/src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java b/src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java index d93066c..e8ca980 100644 --- a/src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java +++ b/src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java @@ -15,6 +15,7 @@ */ package com.android.browser.autocomplete; +import com.android.browser.BrowserSettings; import com.android.browser.SuggestionsAdapter; import com.android.browser.SuggestionsAdapter.SuggestItem; import com.android.browser.autocomplete.SuggestedTextController.TextChangeWatcher; @@ -26,9 +27,9 @@ import android.database.DataSetObserver; import android.graphics.Rect; import android.os.Parcelable; import android.text.Editable; +import android.text.Html; import android.text.Selection; import android.text.TextUtils; -import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.AbsSavedState; @@ -77,7 +78,6 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F private boolean mDropDownDismissedOnCompletion = true; private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; - private boolean mOpenBefore; // Set to true when text is set directly and no filtering shall be performed private boolean mBlockCompletion; @@ -101,6 +101,8 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F public SuggestiveAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + // The completions are always shown in the same color as the hint + // text. mController = new SuggestedTextController(this, getHintTextColors().getDefaultColor()); mPopup = new ListPopupWindow(context, attrs, R.attr.autoCompleteTextViewStyle); @@ -223,7 +225,7 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F return mPopup.getHorizontalOffset(); } - protected void setThreshold(int threshold) { + public void setThreshold(int threshold) { if (threshold <= 0) { threshold = 1; } @@ -341,9 +343,9 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F * triggered. */ private boolean enoughToFilter() { - if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length() + if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getUserText().length() + " threshold=" + mThreshold); - return getText().length() >= mThreshold; + return getUserText().length() >= mThreshold; } /** @@ -351,18 +353,7 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F * to methods on the auto complete text view class so that we can access * private vars without going through thunks. */ - private class MyWatcher implements TextWatcher, TextChangeWatcher { - @Override - public void afterTextChanged(Editable s) { - doAfterTextChanged(); - } - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - doBeforeTextChanged(); - } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } + private class MyWatcher implements TextChangeWatcher { @Override public void onTextChanged(String newText) { doAfterTextChanged(); @@ -376,34 +367,16 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F mBlockCompletion = block; } - void doBeforeTextChanged() { - if (mBlockCompletion) return; - - // when text is changed, inserted or deleted, we attempt to show - // the drop down - mOpenBefore = isPopupShowing(); - if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore); - } - void doAfterTextChanged() { if (DEBUG) Log.d(TAG, "doAfterTextChanged(" + getText() + ")"); if (mBlockCompletion) return; - // if the list was open before the keystroke, but closed afterwards, - // then something in the keystroke processing (an input filter perhaps) - // called performCompletion() and we shouldn't do any more processing. - if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore - + " open=" + isPopupShowing()); - if (mOpenBefore && !isPopupShowing()) { - return; - } - // the drop down is shown only when a minimum number of characters // was typed in the text view if (enoughToFilter()) { if (mFilter != null) { mPopupCanBeUpdated = true; - performFiltering(mController.getUserText(), mLastKeyCode); + performFiltering(getUserText(), mLastKeyCode); buildImeCompletions(); } } else { @@ -413,7 +386,7 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F dismissDropDown(); } if (mFilter != null) { - mFilter.filter(null); + performFiltering(null, mLastKeyCode); } } } @@ -423,7 +396,7 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F * * @return true if the popup menu is showing, false otherwise */ - protected boolean isPopupShowing() { + public boolean isPopupShowing() { return mPopup.isShowing(); } @@ -464,6 +437,20 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F mFilter.filter(text, this); } + protected void performForcedFiltering() { + boolean wasSuspended = false; + if (mController.isCursorHandlingSuspended()) { + mController.resumeCursorMovementHandlingAndApplyChanges(); + wasSuspended = true; + } + + mFilter.filter(getUserText().toString(), this); + + if (wasSuspended) { + mController.suspendCursorMovementHandling(); + } + } + /** * <p>Performs the text completion by converting the selected item from * the drop down list into a string, replacing the text box's content with @@ -553,7 +540,7 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible(); if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter() && - mController.getUserText().length() > 0) { + getUserText().length() > 0) { if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) { showDropDown(); } @@ -760,20 +747,28 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F } } + public String getUserText() { + return mController.getUserText(); + } + private void updateText(SuggestionsAdapter adapter) { - // FIXME: Turn this on only when instant is being used. - // if (!BrowserSettings.getInstance().useInstant()) { - // return; - // } + if (!BrowserSettings.getInstance().useInstant()) { + return; + } if (!isPopupShowing()) { setSuggestedText(null); return; } - if (mAdapter.getCount() > 0 && !TextUtils.isEmpty(mController.getUserText())) { - SuggestItem item = adapter.getItem(0); - setSuggestedText(item.title); + if (mAdapter.getCount() > 0 && !TextUtils.isEmpty(getUserText())) { + for (int i = 0; i < mAdapter.getCount(); ++i) { + SuggestItem current = mAdapter.getItem(i); + if (current.type == SuggestionsAdapter.TYPE_SUGGEST) { + setSuggestedText(current.title); + break; + } + } } } @@ -823,6 +818,17 @@ public class SuggestiveAutoCompleteTextView extends EditText implements Filter.F } public void setSuggestedText(String text) { - mController.setSuggestedText(text); + if (!TextUtils.isEmpty(text)) { + String htmlStripped = Html.fromHtml(text).toString(); + mController.setSuggestedText(htmlStripped); + } else { + mController.setSuggestedText(null); + } + } + + public void getPopupDrawableRect(Rect rect) { + if (mPopup.getListView() != null) { + mPopup.getListView().getDrawingRect(rect); + } } } diff --git a/src/com/android/browser/preferences/LabPreferencesFragment.java b/src/com/android/browser/preferences/LabPreferencesFragment.java index 8a8546f..a06dc3e 100644 --- a/src/com/android/browser/preferences/LabPreferencesFragment.java +++ b/src/com/android/browser/preferences/LabPreferencesFragment.java @@ -18,33 +18,47 @@ package com.android.browser.preferences; import com.android.browser.BrowserActivity; import com.android.browser.BrowserSettings; -import com.android.browser.Controller; import com.android.browser.R; +import com.android.browser.search.SearchEngine; -import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.PreferenceActivity.Header; import android.preference.PreferenceFragment; -import android.preference.PreferenceManager.OnActivityResultListener; - -import java.io.IOException; -import java.io.Serializable; public class LabPreferencesFragment extends PreferenceFragment implements OnPreferenceChangeListener { + private BrowserSettings mBrowserSettings; + private Preference useInstantPref; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mBrowserSettings = BrowserSettings.getInstance(); + // Load the XML preferences file addPreferencesFromResource(R.xml.lab_preferences); Preference e = findPreference(BrowserSettings.PREF_QUICK_CONTROLS); e.setOnPreferenceChangeListener(this); + useInstantPref = findPreference(BrowserSettings.PREF_USE_INSTANT); + } + + @Override + public void onResume() { + super.onResume(); + useInstantPref.setEnabled(false); + + // Enable the "use instant" preference only if the selected + // search engine is google. + if (mBrowserSettings.getSearchEngine() != null) { + final String currentName = mBrowserSettings.getSearchEngine().getName(); + if (SearchEngine.GOOGLE.equals(currentName)) { + useInstantPref.setEnabled(true); + } + } } @Override @@ -54,5 +68,4 @@ public class LabPreferencesFragment extends PreferenceFragment getActivity(), BrowserActivity.class)); return true; } - } diff --git a/src/com/android/browser/search/DefaultSearchEngine.java b/src/com/android/browser/search/DefaultSearchEngine.java index f282b0b..0a7afcf 100644 --- a/src/com/android/browser/search/DefaultSearchEngine.java +++ b/src/com/android/browser/search/DefaultSearchEngine.java @@ -120,4 +120,9 @@ public class DefaultSearchEngine implements SearchEngine { return "ActivitySearchEngine{" + mSearchable + "}"; } + @Override + public boolean wantsEmptyQuery() { + return false; + } + } diff --git a/src/com/android/browser/search/OpenSearchSearchEngine.java b/src/com/android/browser/search/OpenSearchSearchEngine.java index a19e50d..50585c0 100644 --- a/src/com/android/browser/search/OpenSearchSearchEngine.java +++ b/src/com/android/browser/search/OpenSearchSearchEngine.java @@ -295,4 +295,9 @@ public class OpenSearchSearchEngine implements SearchEngine { return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}"; } + @Override + public boolean wantsEmptyQuery() { + return false; + } + } diff --git a/src/com/android/browser/search/SearchEngine.java b/src/com/android/browser/search/SearchEngine.java index b7e1859..3643005 100644 --- a/src/com/android/browser/search/SearchEngine.java +++ b/src/com/android/browser/search/SearchEngine.java @@ -61,4 +61,9 @@ public interface SearchEngine { * Checks whether this search engine supports voice search. */ public boolean supportsVoiceSearch(); + + /** + * Checks whether this search engine should be sent zero char query. + */ + public boolean wantsEmptyQuery(); } diff --git a/src/com/android/browser/search/SearchEngines.java b/src/com/android/browser/search/SearchEngines.java index a6ba3de..a159f17 100644 --- a/src/com/android/browser/search/SearchEngines.java +++ b/src/com/android/browser/search/SearchEngines.java @@ -15,13 +15,11 @@ */ package com.android.browser.search; +import com.android.browser.BrowserSettings; +import com.android.browser.InstantSearchEngine; import com.android.browser.R; -import android.app.SearchManager; -import android.content.ComponentName; import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; @@ -34,6 +32,10 @@ public class SearchEngines { private static final String TAG = "SearchEngines"; public static SearchEngine getDefaultSearchEngine(Context context) { + if (BrowserSettings.getInstance().useInstant()) { + return new InstantSearchEngine(context, DefaultSearchEngine.create(context)); + } + return DefaultSearchEngine.create(context); } |