Implement the psychic search engine.

(a) Add a new subclass of SearchEngine that receives
suggestions provided by psychic and displays them in the
suggestions dropdown.
(b) Add a Labs setting that can turn this feature on or
off.

Change-Id: Icae05b6b55f489278028e5af560d9b36014a0f59
diff --git a/src/com/android/browser/BaseUi.java b/src/com/android/browser/BaseUi.java
index bca999d..15f07c6 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.ObjectAnimator;
 import android.app.Activity;
@@ -690,4 +691,7 @@
         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 @@
     // 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 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 @@
         }
     }
 
+    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 @@
         return useMostVisitedHomepage;
     }
 
+    public boolean useInstant() {
+        return useInstant;
+    }
+
     public boolean showDebugSettings() {
         return showDebugSettings;
     }
@@ -723,6 +739,10 @@
     /* package */void setController(Controller ctrl) {
         mController = ctrl;
         updateTabControlSettings();
+
+        if (mController != null && (searchEngine instanceof InstantSearchEngine)) {
+             ((InstantSearchEngine) searchEngine).setController(mController);
+        }
     }
 
     /*
@@ -902,6 +922,13 @@
             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 6e06e6e..5b655c4 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;
 
@@ -2600,4 +2601,8 @@
         }
     }
 
+    @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.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 @@
 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 @@
     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 @@
                 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 @@
     @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 @@
         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 @@
             }
         }
 
+        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 @@
         }
 
         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 @@
                 notifyDataSetChanged();
             }
         }
+    }
 
+    private int getMaxLines() {
+        int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait;
+        maxLines = (int) Math.ceil(maxLines / 2.0);
+        return maxLines;
     }
 
     /**
@@ -366,11 +376,7 @@
         }
 
         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 @@
             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 @@
                     }
                 }
             } 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 1b33084..f4ba9db 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.ImageButton;
 import android.widget.ImageView;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -274,7 +278,7 @@
     void setFavicon(Bitmap icon) { }
 
     private void clearOrClose() {
-        if (TextUtils.isEmpty(mUrlInput.getText())) {
+        if (TextUtils.isEmpty(mUrlInput.getUserText())) {
             // close
             mUrlInput.clearFocus();
         } else {
@@ -345,7 +349,7 @@
     }
 
     private void updateSearchMode(boolean userEdited) {
-        setSearchMode(!userEdited || TextUtils.isEmpty(mUrlInput.getText()));
+        setSearchMode(!userEdited || TextUtils.isEmpty(mUrlInput.getUserText()));
     }
 
     private void setSearchMode(boolean voiceSearchEnabled) {
@@ -425,4 +429,7 @@
         }
     }
 
+    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 @@
 
     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 @@
     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 362e941..5e6684d 100644
--- a/src/com/android/browser/UrlInputView.java
+++ b/src/com/android/browser/UrlInputView.java
@@ -18,6 +18,7 @@
 
 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 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;
@@ -59,6 +63,7 @@
     private boolean mIncognitoMode;
     private int mVOffset;
     private boolean mNeedsUpdate;
+    private DropdownChangeListener mDropdownListener;
 
     public UrlInputView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
@@ -86,6 +91,22 @@
         setOnItemClickListener(this);
         mVOffset = 0;
         mNeedsUpdate = false;
+        mDropdownListener = null;
+
+        mAdapter.registerDataSetObserver(new DataSetObserver() {
+            @Override
+            public void onChanged() {
+                if (!isPopupShowing()) {
+                    return;
+                }
+                dispatchChange();
+            }
+
+            @Override
+            public void onInvalidated() {
+                dispatchChange();
+            }
+        });
     }
 
     /**
@@ -135,7 +156,7 @@
         mAdapter.setLandscapeMode(mLandscape);
         if (isPopupShowing() && (getVisibility() == View.VISIBLE)) {
             setupDropDown();
-            performFiltering(getText(), 0);
+            performFiltering(getUserText(), 0);
         }
     }
 
@@ -164,12 +185,21 @@
 
     @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();
     }
 
@@ -233,8 +263,7 @@
     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 {
@@ -264,4 +293,17 @@
     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 f914183..5127ff3 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.app.ActionBar;
 import android.app.Activity;
@@ -418,4 +419,8 @@
         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 @@
     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 boolean isCursorHandlingSuspended() {
+        return mSuspended;
+    }
+
     public Parcelable saveInstanceState(Parcelable superState) {
         assertNotIgnoringSelectionChanges();
         SavedState ss = new SavedState(superState);
@@ -160,6 +166,7 @@
         assertNotIgnoringSelectionChanges();
         Editable buffer = mTextOwner.getText();
         mTextSelectionBeforeIgnoringChanges = new BufferSelection(buffer);
+        mSuspended = true;
     }
 
     /**
@@ -181,6 +188,7 @@
                     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.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 @@
     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 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 @@
         return mPopup.getHorizontalOffset();
     }
 
-    protected void setThreshold(int threshold) {
+    public void setThreshold(int threshold) {
         if (threshold <= 0) {
             threshold = 1;
         }
@@ -341,9 +343,9 @@
      * 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 @@
      * 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 @@
         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 @@
                 dismissDropDown();
             }
             if (mFilter != null) {
-                mFilter.filter(null);
+                performFiltering(null, mLastKeyCode);
             }
         }
     }
@@ -423,7 +396,7 @@
      *
      * @return true if the popup menu is showing, false otherwise
      */
-    protected boolean isPopupShowing() {
+    public boolean isPopupShowing() {
         return mPopup.isShowing();
     }
 
@@ -464,6 +437,20 @@
         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 @@
 
         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 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 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 @@
 
 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 @@
                 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 @@
         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 @@
         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 @@
      * 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 @@
     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);
     }