Add autocomplete to the browser UrlInputView.

Code and tests based on the google search app. A lot
less code would be duplicated if we could somehow
override AutoCompleteTextView but that is made impossible
by it calling a bunch of stuff in its constructor. To do
so would require changes to the existing API.

I've verified that the unit test passes, but other browser
tests appear to fail - even on a clean branch with none
of my changes.

Also fixes a minor bug in SearchEngines.getSearchableInfo( ).

Change-Id: Ic61bc6b8fa27cd210a45dc181ebf15accf503244
diff --git a/src/com/android/browser/SuggestionsAdapter.java b/src/com/android/browser/SuggestionsAdapter.java
index 6e55539..3636bbf 100644
--- a/src/com/android/browser/SuggestionsAdapter.java
+++ b/src/com/android/browser/SuggestionsAdapter.java
@@ -392,11 +392,11 @@
     /**
      * data object to hold suggestion values
      */
-    class SuggestItem {
-        String title;
-        String url;
-        int type;
-        String extra;
+    public class SuggestItem {
+        public String title;
+        public String url;
+        public int type;
+        public String extra;
 
         public SuggestItem(String text, String u, int t) {
             title = text;
diff --git a/src/com/android/browser/TitleBarXLarge.java b/src/com/android/browser/TitleBarXLarge.java
index 85935a0..1b33084 100644
--- a/src/com/android/browser/TitleBarXLarge.java
+++ b/src/com/android/browser/TitleBarXLarge.java
@@ -16,6 +16,7 @@
 
 package com.android.browser;
 
+import com.android.browser.autocomplete.SuggestedTextController.TextChangeWatcher;
 import com.android.browser.search.SearchEngine;
 
 import android.app.Activity;
@@ -23,10 +24,7 @@
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
-import android.text.Editable;
 import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -45,8 +43,7 @@
  * tabbed title bar for xlarge screen browser
  */
 public class TitleBarXLarge extends TitleBarBase
-        implements OnClickListener, OnFocusChangeListener,
-    TextWatcher {
+        implements OnClickListener, OnFocusChangeListener, TextChangeWatcher {
 
     private XLargeUi mUi;
 
@@ -136,7 +133,7 @@
         mUrlInput.setController(mUiController);
         mUrlInput.setOnFocusChangeListener(this);
         mUrlInput.setSelectAllOnFocus(true);
-        mUrlInput.addTextChangedListener(this);
+        mUrlInput.addQueryTextWatcher(this);
         setFocusState(false);
     }
 
@@ -372,7 +369,7 @@
     // UrlInput text watcher
 
     @Override
-    public void afterTextChanged(Editable s) {
+    public void onTextChanged(String newText) {
         if (mUrlInput.hasFocus()) {
             // check if input field is empty and adjust voice search state
             updateSearchMode(true);
@@ -381,14 +378,6 @@
         }
     }
 
-    @Override
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-    }
-
-    @Override
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-    }
-
     // voicesearch
 
     @Override
diff --git a/src/com/android/browser/UrlInputView.java b/src/com/android/browser/UrlInputView.java
index 2ec2111..362e941 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.autocomplete.SuggestiveAutoCompleteTextView;
 import com.android.browser.search.SearchEngine;
 import com.android.browser.search.SearchEngineInfo;
 import com.android.browser.search.SearchEngines;
@@ -32,7 +33,6 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
-import android.widget.AutoCompleteTextView;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
 
@@ -42,7 +42,7 @@
  * url/search input view
  * handling suggestions
  */
-public class UrlInputView extends AutoCompleteTextView
+public class UrlInputView extends SuggestiveAutoCompleteTextView
         implements OnEditorActionListener,
         CompletionListener, OnItemClickListener {
 
@@ -261,4 +261,7 @@
         return super.onKeyDown(keyCode, evt);
     }
 
+    public SuggestionsAdapter getAdapter() {
+        return mAdapter;
+    }
 }
diff --git a/src/com/android/browser/autocomplete/SuggestedSpan.java b/src/com/android/browser/autocomplete/SuggestedSpan.java
new file mode 100644
index 0000000..dc04cb2
--- /dev/null
+++ b/src/com/android/browser/autocomplete/SuggestedSpan.java
@@ -0,0 +1,34 @@
+/*
+ * 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.autocomplete;
+
+import android.os.Parcel;
+import android.text.style.ForegroundColorSpan;
+
+/**
+ * Class used to mark the portion of text within {@link SuggestiveEditText} that is suggested.
+ */
+class SuggestedSpan extends ForegroundColorSpan {
+
+    public SuggestedSpan(Parcel src) {
+        super(src);
+    }
+
+    public SuggestedSpan(int color) {
+        super(color);
+    }
+
+}
diff --git a/src/com/android/browser/autocomplete/SuggestedTextController.java b/src/com/android/browser/autocomplete/SuggestedTextController.java
new file mode 100644
index 0000000..e9b74ce
--- /dev/null
+++ b/src/com/android/browser/autocomplete/SuggestedTextController.java
@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2010 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.autocomplete;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+
+import java.util.ArrayList;
+
+import junit.framework.Assert;
+
+
+/**
+ * The query editor can show a suggestion, grayed out following the query that the user has
+ * entered so far. As the user types new characters, these should replace the grayed suggestion
+ * text. This class manages this logic, displaying the suggestion when the user entered text is a
+ * prefix of it, and hiding it otherwise.
+ *
+ * Note, the text in the text view will contain the entire suggestion, not just what the user
+ * entered. Instead of retrieving the text from the text view, {@link #getUserText()} should be
+ * called on this class.
+ */
+public class SuggestedTextController {
+    private static final boolean DBG = false;
+    private static final String TAG = "Browser.SuggestedTextController";
+
+    private final BufferTextWatcher mBufferTextWatcher = new BufferTextWatcher();
+    private final BufferSpanWatcher mBufferSpanWatcher = new BufferSpanWatcher();
+    private final ArrayList<TextChangeWatcher> mTextWatchers;
+    private final TextOwner mTextOwner;
+    private final StringBuffer mUserEntered;
+    private final SuggestedSpan mSuggested;
+    private String mSuggestedText;
+    private TextChangeAttributes mCurrentTextChange;
+    /**
+     * 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.
+     */
+    private BufferSelection mTextSelectionBeforeIgnoringChanges;
+
+    public SuggestedTextController(final EditText textView, int color) {
+        this(new TextOwner() {
+            @Override
+            public Editable getText() {
+                return textView.getText();
+            }
+            @Override
+            public void addTextChangedListener(TextWatcher watcher) {
+                textView.addTextChangedListener(watcher);
+            }
+            @Override
+            public void removeTextChangedListener(TextWatcher watcher) {
+                textView.removeTextChangedListener(watcher);
+            }
+            @Override
+            public void setText(String text) {
+                textView.setText(text);
+            }
+        }, color);
+    }
+
+    private void initialize(String userText, int selStart, int selEnd, String suggested) {
+        Editable text = mTextOwner.getText();
+
+        if (userText == null) userText = "";
+        String allText = userText;
+        int suggestedStart = allText.length();
+        if (suggested != null && userText != null) {
+            if (suggested.startsWith(userText.toLowerCase())) {
+                allText = suggested;
+            }
+        }
+
+        // allText is at this point either "userText" (not null) or
+        // "suggested" if thats not null and starts with userText.
+        text.replace(0, text.length(), allText);
+        Selection.setSelection(text, selStart, selEnd);
+        mUserEntered.replace(0, mUserEntered.length(), userText);
+        mSuggestedText = suggested;
+        if (suggestedStart < text.length()) {
+            text.setSpan(mSuggested, suggestedStart, text.length(),
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        } else {
+            text.removeSpan(mSuggested);
+        }
+        text.setSpan(mBufferSpanWatcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+        mTextOwner.addTextChangedListener(mBufferTextWatcher);
+        if (DBG) checkInvariant(text);
+    }
+
+    private void assertNotIgnoringSelectionChanges() {
+        if (mTextSelectionBeforeIgnoringChanges != null) {
+            throw new IllegalStateException(
+                    "Illegal operation while cursor movement processing suspended");
+        }
+    }
+
+    public Parcelable saveInstanceState(Parcelable superState) {
+        assertNotIgnoringSelectionChanges();
+        SavedState ss = new SavedState(superState);
+        Editable buffer = mTextOwner.getText();
+        ss.mUserText = getUserText();
+        ss.mSuggestedText = mSuggestedText;
+        ss.mSelStart = Selection.getSelectionStart(buffer);
+        ss.mSelEnd = Selection.getSelectionEnd(buffer);
+        return ss;
+    }
+
+    public Parcelable restoreInstanceState(Parcelable state) {
+        assertNotIgnoringSelectionChanges();
+        if (!(state instanceof SavedState)) return state;
+        SavedState ss = (SavedState) state;
+        if (DBG) {
+            Log.d(TAG, "restoreInstanceState t='" + ss.mUserText + "' suggestion='" +
+                    ss.mSuggestedText + " sel=" + ss.mSelStart + ".." + ss.mSelEnd);
+        }
+        // remove our listeners so we don't get notifications while re-initialising
+        mTextOwner.getText().removeSpan(mBufferSpanWatcher);
+        mTextOwner.removeTextChangedListener(mBufferTextWatcher);
+        // and initialise will re-add the watchers
+        initialize(ss.mUserText, ss.mSelStart, ss.mSelEnd, ss.mSuggestedText);
+        notifyUserEnteredChanged();
+        return ss.getSuperState();
+    }
+
+    /**
+     * Temporarily stop processing cursor movements and selection changes. While cursor movements
+     * are being ignored, the text in the buffer must NOT be changed; doing so will result in an
+     * {@link IllegalStateException} being thrown.
+     *
+     * To stop ignoring cursor movements, call
+     * {@link #resumeCursorMovementHandlingAndApplyChanges()}.
+     */
+    public void suspendCursorMovementHandling() {
+        assertNotIgnoringSelectionChanges();
+        Editable buffer = mTextOwner.getText();
+        mTextSelectionBeforeIgnoringChanges = new BufferSelection(buffer);
+    }
+
+    /**
+     * Start responding to cursor movements and selection changes again. If the cursor or selection
+     * moved while it was being ignored, these changes will be processed now.
+     */
+    public void resumeCursorMovementHandlingAndApplyChanges() {
+        Editable buffer = mTextOwner.getText();
+        BufferSelection oldSelection = mTextSelectionBeforeIgnoringChanges;
+        mTextSelectionBeforeIgnoringChanges = null;
+        BufferSelection newSelection = new BufferSelection(buffer);
+        if (oldSelection.mStart != newSelection.mStart) {
+            mBufferSpanWatcher.onSpanChanged(buffer, Selection.SELECTION_START,
+                    oldSelection.mStart, oldSelection.mStart,
+                    newSelection.mStart, newSelection.mStart);
+        }
+        if (oldSelection.mEnd != newSelection.mEnd) {
+            mBufferSpanWatcher.onSpanChanged(buffer, Selection.SELECTION_END,
+                    oldSelection.mEnd, oldSelection.mEnd,
+                    newSelection.mEnd, newSelection.mEnd);
+        }
+    }
+
+    /**
+     * Sets the current suggested text. A portion of this will be added to the user entered text if
+     * that is a prefix of the suggestion.
+     */
+    public void setSuggestedText(String text) {
+        assertNotIgnoringSelectionChanges();
+        if (!TextUtils.equals(text, mSuggestedText)) {
+            if (DBG) Log.d(TAG, "setSuggestedText(" + text + ")");
+            mSuggestedText = text;
+            if (mCurrentTextChange == null) {
+                mCurrentTextChange = new TextChangeAttributes(0, 0, 0);
+                Editable buffer = mTextOwner.getText();
+                handleTextChanged(buffer);
+            }
+        }
+    }
+
+    /**
+     * Gets the portion of displayed text that is not suggested.
+     */
+    public String getUserText() {
+        assertNotIgnoringSelectionChanges();
+        return mUserEntered.toString();
+    }
+
+    /**
+     * Sets the given text as if it has been entered by the user.
+     */
+    public void setText(String text) {
+        assertNotIgnoringSelectionChanges();
+        if (text == null) text = "";
+        Editable buffer = mTextOwner.getText();
+        buffer.removeSpan(mSuggested);
+        // this will cause a handleTextChanged call
+        buffer.replace(0, text.length(), text);
+    }
+
+    public void addUserTextChangeWatcher(TextChangeWatcher watcher) {
+        mTextWatchers.add(watcher);
+    }
+
+    private void handleTextChanged(Editable newText) {
+        // When we make changes to the buffer from within this function, it results in recursive
+        // calls to beforeTextChanges(), afterTextChanged(). We want to ignore the changes we're
+        // making ourself:
+        if (mCurrentTextChange.isHandled()) return;
+        mCurrentTextChange.setHandled();
+        final int pos = mCurrentTextChange.mPos;
+        final int countBefore = mCurrentTextChange.mCountBefore;
+        final int countAfter = mCurrentTextChange.mCountAfter;
+        final int cursorPos = Selection.getSelectionEnd(newText);
+        if (DBG) {
+            Log.d(TAG, "pos=" + pos +"; countBefore=" + countBefore + "; countAfter=" +
+                    countAfter + "; cursor=" + cursorPos);
+        }
+        mUserEntered.replace(pos, pos + countBefore,
+                newText.subSequence(pos, pos + countAfter).toString());
+        if (DBG) Log.d(TAG, "User entered: '" + mUserEntered + "' all='" + newText + "'");
+        final int userLen = mUserEntered.length();
+        boolean haveSuggested = newText.getSpanStart(mSuggested) != -1;
+        if (mSuggestedText != null &&
+                mSuggestedText.startsWith(mUserEntered.toString().toLowerCase())) {
+            if (haveSuggested) {
+                if (!mSuggestedText.equalsIgnoreCase(newText.toString())) {
+                    if (countAfter > countBefore) {
+                        // net insertion
+                        int len = countAfter - countBefore;
+                        newText.delete(pos + len, pos + len + len);
+                    } else {
+                        // net deletion
+                        newText.replace(userLen, newText.length(),
+                                mSuggestedText.substring(userLen));
+                        if (countBefore == 0) {
+                            // no change to the text - likely just suggested change
+                            Selection.setSelection(newText, cursorPos);
+                        }
+                    }
+                }
+            } else {
+                // no current suggested text - add it
+                newText.insert(userLen, mSuggestedText.substring(userLen));
+                // keep the cursor at the end of the user entered text, if that where it was
+                // before.
+                if (cursorPos == userLen) {
+                    Selection.setSelection(newText, userLen);
+                }
+            }
+            if (userLen == newText.length()) {
+                newText.removeSpan(mSuggested);
+            } else {
+                newText.setSpan(mSuggested, userLen, newText.length(),
+                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+            }
+        } else {
+            if (newText.getSpanStart(mSuggested) != -1) {
+                newText.removeSpan(mSuggested);
+                newText.delete(mUserEntered.length(), newText.length());
+            }
+        }
+        if (DBG) checkInvariant(newText);
+        mCurrentTextChange = null;
+        if (countBefore > 0 || countAfter > 0) {
+            notifyUserEnteredChanged();
+        }
+    }
+
+    private void notifyUserEnteredChanged() {
+        for (TextChangeWatcher watcher : mTextWatchers) {
+            watcher.onTextChanged(mUserEntered.toString());
+        }
+    }
+
+    /**
+     * Basic interface for being notified of changes to some text.
+     */
+    public interface TextChangeWatcher {
+        void onTextChanged(String newText);
+    }
+
+    /**
+     * Interface class to wrap required methods from {@link EditText}, or some other class used
+     * to test without needing an @{link EditText}.
+     */
+    public interface TextOwner {
+        Editable getText();
+        void addTextChangedListener(TextWatcher watcher);
+        void removeTextChangedListener(TextWatcher watcher);
+        void setText(String text);
+    }
+
+    /**
+     * This class stores the parameters passed to {@link BufferTextWatcher#beforeTextChanged},
+     * together with a flag indicating if this invocation has been dealt with yet. We need this
+     * information, together with the parameters passed to
+     * {@link BufferTextWatcher#afterTextChanged}, to restore our internal state when the buffer is
+     * edited.
+     *
+     * Since the changes we make from within {@link BufferTextWatcher#afterTextChanged} also trigger
+     * further recursive calls to {@link BufferTextWatcher#beforeTextChanged} and
+     * {@link BufferTextWatcher#afterTextChanged}, this class helps detect these recursive calls so
+     * they can be ignored.
+     */
+    private static class TextChangeAttributes {
+        public final int mPos;
+        public final int mCountAfter;
+        public final int mCountBefore;
+        private boolean mHandled;
+
+        public TextChangeAttributes(int pos, int countAfter, int countBefore) {
+            mPos = pos;
+            mCountAfter = countAfter;
+            mCountBefore = countBefore;
+        }
+
+        public void setHandled() {
+            mHandled = true;
+        }
+
+        public boolean isHandled() {
+            return mHandled;
+        }
+    }
+
+    /**
+     * Encapsulates the state of the text selection (and cursor) within a text buffer.
+     */
+    private static class BufferSelection {
+        final int mStart;
+        final int mEnd;
+        public BufferSelection(CharSequence text) {
+            mStart = Selection.getSelectionStart(text);
+            mEnd = Selection.getSelectionEnd(text);
+        }
+        @Override
+        public boolean equals(Object other) {
+            if (!(other instanceof BufferSelection)) return super.equals(other);
+            BufferSelection otherSel = (BufferSelection) other;
+            return this.mStart == otherSel.mStart && this.mEnd == otherSel.mEnd;
+        }
+    }
+
+    private class BufferTextWatcher implements TextWatcher {
+        @Override
+        public void afterTextChanged(Editable newText) {
+            if (DBG) {
+                Log.d(TAG, "afterTextChanged('" + newText + "')");
+            }
+            assertNotIgnoringSelectionChanges();
+            handleTextChanged(newText);
+        }
+
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            assertNotIgnoringSelectionChanges();
+            if (mCurrentTextChange == null) {
+                mCurrentTextChange = new TextChangeAttributes(start, after, count);
+            }
+        }
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+        }
+    }
+
+    private class BufferSpanWatcher implements SpanWatcher {
+        @Override
+        public void onSpanAdded(Spannable text, Object what, int start, int end) {
+        }
+
+        @Override
+        public void onSpanChanged(
+                Spannable text, Object what, int ostart, int oend, int nstart, int nend) {
+            if (mCurrentTextChange != null) return;
+            if (mTextSelectionBeforeIgnoringChanges != null) return;
+            if (what == Selection.SELECTION_END) {
+                if (DBG) Log.d(TAG, "cursor move to " + nend);
+                if (nend > mUserEntered.length()) {
+                    mUserEntered.replace(0, mUserEntered.length(), text.toString());
+                    text.removeSpan(mSuggested);
+                }
+                if (DBG) checkInvariant(text);
+            }
+        }
+
+        @Override
+        public void onSpanRemoved(Spannable text, Object what, int start, int end) {
+        }
+    }
+
+    public static class SavedState extends View.BaseSavedState {
+        String mUserText;
+        String mSuggestedText;
+        int mSelStart;
+        int mSelEnd;
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeString(mUserText);
+            out.writeString(mSuggestedText);
+            out.writeInt(mSelStart);
+            out.writeInt(mSelEnd);
+        }
+
+        @SuppressWarnings("hiding")
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+
+        private SavedState(Parcel in) {
+            super(in);
+            mUserText = in.readString();
+            mSuggestedText = in.readString();
+            mSelStart = in.readInt();
+            mSelEnd = in.readInt();
+        }
+    }
+
+    /*
+     * The remaining functions are used for testing purposes only.
+     * -----------------------------------------------------------
+     */
+
+    /**
+     * Verify that the internal state of this class is consistent.
+     */
+    @VisibleForTesting
+    void checkInvariant(final Spannable s) {
+        int suggestedStart = s.getSpanStart(mSuggested);
+        int suggestedEnd = s.getSpanEnd(mSuggested);
+        int cursorPos = Selection.getSelectionEnd(s);
+        if (suggestedStart == -1 || suggestedEnd == -1) {
+            suggestedStart = suggestedEnd = s.length();
+        }
+        String userEntered = getUserText();
+        Log.d(TAG, "checkInvariant all='" + s + "' (len " + s.length() + ") sug="
+                + suggestedStart + ".." + suggestedEnd + " cursor=" + cursorPos +
+                " ue='" + userEntered + "' (len " + userEntered.length() + ")");
+        int suggestedLen = suggestedEnd - suggestedStart;
+        Assert.assertEquals("Sum of user and suggested text lengths doesn't match total length",
+                s.length(), userEntered.length() + suggestedLen);
+        Assert.assertEquals("End of user entered text doesn't match start of suggested",
+                suggestedStart, userEntered.length());
+        Assert.assertTrue("user entered text does not match start of buffer",
+                userEntered.toString().equalsIgnoreCase(
+                        s.subSequence(0, suggestedStart).toString()));
+        if (mSuggestedText != null && suggestedStart < s.length()) {
+            Assert.assertTrue("User entered is not a prefix of suggested",
+                    mSuggestedText.startsWith(userEntered.toString().toLowerCase()));
+            Assert.assertTrue("Suggested text does not match buffer contents",
+                    mSuggestedText.equalsIgnoreCase(s.toString().toLowerCase()));
+        }
+        if (mSuggestedText == null) {
+            Assert.assertEquals("Non-zero suggention length with null suggestion", 0, suggestedLen);
+        } else {
+            Assert.assertTrue("Suggestion text longer than suggestion (" + mSuggestedText.length() +
+                    ">" + suggestedLen + ")", suggestedLen <= mSuggestedText.length());
+        }
+        Assert.assertTrue("Cursor within suggested part", cursorPos <= suggestedStart);
+    }
+
+    @VisibleForTesting
+    SuggestedTextController(TextOwner textOwner, int color) {
+        mUserEntered = new StringBuffer();
+        mSuggested = new SuggestedSpan(color);
+        mTextOwner = textOwner;
+        mTextWatchers = new ArrayList<TextChangeWatcher>();
+        initialize(null, 0, 0, null);
+    }
+}
diff --git a/src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java b/src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java
new file mode 100644
index 0000000..d93066c
--- /dev/null
+++ b/src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java
@@ -0,0 +1,828 @@
+/*
+ * 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.autocomplete;
+
+import com.android.browser.SuggestionsAdapter;
+import com.android.browser.SuggestionsAdapter.SuggestItem;
+import com.android.browser.autocomplete.SuggestedTextController.TextChangeWatcher;
+import com.android.internal.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.os.Parcelable;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.AbsSavedState;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ListAdapter;
+import android.widget.ListPopupWindow;
+import android.widget.TextView;
+
+
+/**
+ * This is a stripped down version of the framework AutoCompleteTextView
+ * class with added support for displaying completions in-place. Note that
+ * this cannot be implemented as a subclass of the above without making
+ * substantial changes to it and its descendants.
+ *
+ * @see android.widget.AutoCompleteTextView
+ */
+public class SuggestiveAutoCompleteTextView extends EditText implements Filter.FilterListener {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "SuggestiveAutoCompleteTextView";
+
+    private CharSequence mHintText;
+    private TextView mHintView;
+    private int mHintResource;
+
+    private SuggestionsAdapter mAdapter;
+    private Filter mFilter;
+    private int mThreshold;
+
+    private ListPopupWindow mPopup;
+    private int mDropDownAnchorId;
+
+    private AdapterView.OnItemClickListener mItemClickListener;
+
+    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;
+
+    // When set, an update in the underlying adapter will update the result list popup.
+    // Set to false when the list is hidden to prevent asynchronous updates to popup the list again.
+    private boolean mPopupCanBeUpdated = true;
+
+    private PassThroughClickListener mPassThroughClickListener;
+    private PopupDataSetObserver mObserver;
+    private SuggestedTextController mController;
+
+    public SuggestiveAutoCompleteTextView(Context context) {
+        this(context, null);
+    }
+
+    public SuggestiveAutoCompleteTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.autoCompleteTextViewStyle);
+    }
+
+    public SuggestiveAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mController = new SuggestedTextController(this, getHintTextColors().getDefaultColor());
+        mPopup = new ListPopupWindow(context, attrs,
+                 R.attr.autoCompleteTextViewStyle);
+        mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+        mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW);
+
+        TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.AutoCompleteTextView, defStyle, 0);
+
+        mThreshold = a.getInt(
+                R.styleable.AutoCompleteTextView_completionThreshold, 2);
+
+        mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector));
+        mPopup.setVerticalOffset((int)
+                a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f));
+        mPopup.setHorizontalOffset((int)
+                a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f));
+
+        // Get the anchor's id now, but the view won't be ready, so wait to actually get the
+        // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later.
+        // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return
+        // this TextView, as a default anchoring point.
+        mDropDownAnchorId = a.getResourceId(
+                R.styleable.AutoCompleteTextView_dropDownAnchor, View.NO_ID);
+
+        // For dropdown width, the developer can specify a specific width, or MATCH_PARENT
+        // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view).
+        mPopup.setWidth(a.getLayoutDimension(
+                R.styleable.AutoCompleteTextView_dropDownWidth,
+                ViewGroup.LayoutParams.WRAP_CONTENT));
+        mPopup.setHeight(a.getLayoutDimension(
+                R.styleable.AutoCompleteTextView_dropDownHeight,
+                ViewGroup.LayoutParams.WRAP_CONTENT));
+
+        mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
+                R.layout.simple_dropdown_hint);
+
+        mPopup.setOnItemClickListener(new DropDownItemClickListener());
+        setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint));
+
+        // Always turn on the auto complete input type flag, since it
+        // makes no sense to use this widget without it.
+        int inputType = getInputType();
+        if ((inputType&EditorInfo.TYPE_MASK_CLASS)
+                == EditorInfo.TYPE_CLASS_TEXT) {
+            inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+            setRawInputType(inputType);
+        }
+
+        a.recycle();
+
+        setFocusable(true);
+
+        mController.addUserTextChangeWatcher(new MyWatcher());
+
+        mPassThroughClickListener = new PassThroughClickListener();
+        super.setOnClickListener(mPassThroughClickListener);
+    }
+
+    @Override
+    public void setOnClickListener(OnClickListener listener) {
+        mPassThroughClickListener.mWrapped = listener;
+    }
+
+    /**
+     * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
+     */
+    private void onClickImpl() {
+        // If the dropdown is showing, bring the keyboard to the front
+        // when the user touches the text field.
+        if (isPopupShowing()) {
+            ensureImeVisible(true);
+        }
+    }
+
+    /**
+     * <p>Sets the optional hint text that is displayed at the bottom of the
+     * the matching list.  This can be used as a cue to the user on how to
+     * best use the list, or to provide extra information.</p>
+     *
+     * @param hint the text to be displayed to the user
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+     */
+    private void setCompletionHint(CharSequence hint) {
+        mHintText = hint;
+        if (hint != null) {
+            if (mHintView == null) {
+                final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate(
+                        mHintResource, null).findViewById(R.id.text1);
+                hintView.setText(mHintText);
+                mHintView = hintView;
+                mPopup.setPromptView(hintView);
+            } else {
+                mHintView.setText(hint);
+            }
+        } else {
+            mPopup.setPromptView(null);
+            mHintView = null;
+        }
+    }
+
+    protected int getDropDownWidth() {
+        return mPopup.getWidth();
+    }
+
+    public void setDropDownWidth(int width) {
+        mPopup.setWidth(width);
+    }
+
+    protected void setDropDownVerticalOffset(int offset) {
+        mPopup.setVerticalOffset(offset);
+    }
+
+    public void setDropDownHorizontalOffset(int offset) {
+        mPopup.setHorizontalOffset(offset);
+    }
+
+    protected int getDropDownHorizontalOffset() {
+        return mPopup.getHorizontalOffset();
+    }
+
+    protected void setThreshold(int threshold) {
+        if (threshold <= 0) {
+            threshold = 1;
+        }
+
+        mThreshold = threshold;
+    }
+
+    protected void setOnItemClickListener(AdapterView.OnItemClickListener l) {
+        mItemClickListener = l;
+    }
+
+    public void setAdapter(SuggestionsAdapter adapter) {
+        if (mObserver == null) {
+            mObserver = new PopupDataSetObserver();
+        } else if (mAdapter != null) {
+            mAdapter.unregisterDataSetObserver(mObserver);
+        }
+        mAdapter = adapter;
+        if (mAdapter != null) {
+            mFilter = mAdapter.getFilter();
+            adapter.registerDataSetObserver(mObserver);
+        } else {
+            mFilter = null;
+        }
+
+        mPopup.setAdapter(mAdapter);
+    }
+
+    @Override
+    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
+                && !mPopup.isDropDownAlwaysVisible()) {
+            // special case for the back key, we do not even try to send it
+            // to the drop down list but instead, consume it immediately
+            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+                KeyEvent.DispatcherState state = getKeyDispatcherState();
+                if (state != null) {
+                    state.startTracking(event, this);
+                }
+                return true;
+            } else if (event.getAction() == KeyEvent.ACTION_UP) {
+                KeyEvent.DispatcherState state = getKeyDispatcherState();
+                if (state != null) {
+                    state.handleUpEvent(event);
+                }
+                if (event.isTracking() && !event.isCanceled()) {
+                    dismissDropDown();
+                    return true;
+                }
+            }
+        }
+        return super.onKeyPreIme(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        boolean consumed = mPopup.onKeyUp(keyCode, event);
+        if (consumed) {
+            switch (keyCode) {
+            // if the list accepts the key events and the key event
+            // was a click, the text view gets the selected item
+            // from the drop down as its content
+            case KeyEvent.KEYCODE_ENTER:
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+            case KeyEvent.KEYCODE_TAB:
+                if (event.hasNoModifiers()) {
+                    performCompletion();
+                }
+                return true;
+            }
+        }
+
+        if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
+            performCompletion();
+            return true;
+        }
+
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mPopup.onKeyDown(keyCode, event)) {
+            return true;
+        }
+
+        if (!isPopupShowing()) {
+            switch(keyCode) {
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (event.hasNoModifiers()) {
+                    performValidation();
+                }
+            }
+        }
+
+        if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
+            return true;
+        }
+
+        mLastKeyCode = keyCode;
+        boolean handled = super.onKeyDown(keyCode, event);
+        mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+
+        if (handled && isPopupShowing()) {
+            clearListSelection();
+        }
+
+        return handled;
+    }
+
+    /**
+     * Returns <code>true</code> if the amount of text in the field meets
+     * or exceeds the {@link #getThreshold} requirement.  You can override
+     * this to impose a different standard for when filtering will be
+     * triggered.
+     */
+    private boolean enoughToFilter() {
+        if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
+                + " threshold=" + mThreshold);
+        return getText().length() >= mThreshold;
+    }
+
+    /**
+     * This is used to watch for edits to the text view.  Note that we call
+     * 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) {
+        }
+        @Override
+        public void onTextChanged(String newText) {
+            doAfterTextChanged();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    protected void setBlockCompletion(boolean block) {
+        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);
+                buildImeCompletions();
+            }
+        } else {
+            // drop down is automatically dismissed when enough characters
+            // are deleted from the text view
+            if (!mPopup.isDropDownAlwaysVisible()) {
+                dismissDropDown();
+            }
+            if (mFilter != null) {
+                mFilter.filter(null);
+            }
+        }
+    }
+
+    /**
+     * <p>Indicates whether the popup menu is showing.</p>
+     *
+     * @return true if the popup menu is showing, false otherwise
+     */
+    protected boolean isPopupShowing() {
+        return mPopup.isShowing();
+    }
+
+    /**
+     * <p>Converts the selected item from the drop down list into a sequence
+     * of character that can be used in the edit box.</p>
+     *
+     * @param selectedItem the item selected by the user for completion
+     *
+     * @return a sequence of characters representing the selected suggestion
+     */
+    protected CharSequence convertSelectionToString(Object selectedItem) {
+        return mFilter.convertResultToString(selectedItem);
+    }
+
+    /**
+     * <p>Clear the list selection.  This may only be temporary, as user input will often bring
+     * it back.
+     */
+    private void clearListSelection() {
+        mPopup.clearListSelection();
+    }
+
+    /**
+     * <p>Starts filtering the content of the drop down list. The filtering
+     * pattern is the content of the edit box. Subclasses should override this
+     * method to filter with a different pattern, for instance a substring of
+     * <code>text</code>.</p>
+     *
+     * @param text the filtering pattern
+     * @param keyCode the last character inserted in the edit box; beware that
+     * this will be null when text is being added through a soft input method.
+     */
+    @SuppressWarnings({ "UnusedDeclaration" })
+    protected void performFiltering(CharSequence text, int keyCode) {
+        if (DEBUG) Log.d(TAG, "performFiltering(" + text + ")");
+
+        mFilter.filter(text, this);
+    }
+
+    /**
+     * <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
+     * this string and finally dismissing the drop down menu.</p>
+     */
+    private void performCompletion() {
+        performCompletion(null, -1, -1);
+    }
+
+    @Override
+    public void onCommitCompletion(CompletionInfo completion) {
+        if (isPopupShowing()) {
+            mBlockCompletion = true;
+            replaceText(completion.getText());
+            mBlockCompletion = false;
+
+            mPopup.performItemClick(completion.getPosition());
+        }
+    }
+
+    private void performCompletion(View selectedView, int position, long id) {
+        if (isPopupShowing()) {
+            Object selectedItem;
+            if (position < 0) {
+                selectedItem = mPopup.getSelectedItem();
+            } else {
+                selectedItem = mAdapter.getItem(position);
+            }
+            if (selectedItem == null) {
+                Log.w(TAG, "performCompletion: no selected item");
+                return;
+            }
+
+            mBlockCompletion = true;
+            replaceText(convertSelectionToString(selectedItem));
+            mBlockCompletion = false;
+
+            if (mItemClickListener != null) {
+                final ListPopupWindow list = mPopup;
+
+                if (selectedView == null || position < 0) {
+                    selectedView = list.getSelectedView();
+                    position = list.getSelectedItemPosition();
+                    id = list.getSelectedItemId();
+                }
+                mItemClickListener.onItemClick(list.getListView(), selectedView, position, id);
+            }
+        }
+
+        if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) {
+            dismissDropDown();
+        }
+    }
+
+    /**
+     * <p>Performs the text completion by replacing the current text by the
+     * selected item. Subclasses should override this method to avoid replacing
+     * the whole content of the edit box.</p>
+     *
+     * @param text the selected suggestion in the drop down list
+     */
+    protected void replaceText(CharSequence text) {
+        clearComposingText();
+
+        setText(text);
+        // make sure we keep the caret at the end of the text view
+        Editable spannable = getText();
+        Selection.setSelection(spannable, spannable.length());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onFilterComplete(int count) {
+        updateDropDownForFilter(count);
+    }
+
+    private void updateDropDownForFilter(int count) {
+        // Not attached to window, don't update drop-down
+        if (getWindowVisibility() == View.GONE) return;
+
+        /*
+         * This checks enoughToFilter() again because filtering requests
+         * are asynchronous, so the result may come back after enough text
+         * has since been deleted to make it no longer appropriate
+         * to filter.
+         */
+
+        final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible();
+        if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter() &&
+                mController.getUserText().length() > 0) {
+            if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) {
+                showDropDown();
+            }
+        } else if (!dropDownAlwaysVisible && isPopupShowing()) {
+            dismissDropDown();
+            // When the filter text is changed, the first update from the adapter may show an empty
+            // count (when the query is being performed on the network). Future updates when some
+            // content has been retrieved should still be able to update the list.
+            mPopupCanBeUpdated = true;
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) {
+            dismissDropDown();
+        }
+    }
+
+    @Override
+    protected void onDisplayHint(int hint) {
+        super.onDisplayHint(hint);
+        switch (hint) {
+            case INVISIBLE:
+                if (!mPopup.isDropDownAlwaysVisible()) {
+                    dismissDropDown();
+                }
+                break;
+        }
+    }
+
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        // TextView makes several cursor movements when gaining focus, and this interferes with
+        // the suggested vs user entered text. Tell the controller to temporarily ignore cursor
+        // movements while this is going on.
+        mController.suspendCursorMovementHandling();
+
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+        // Perform validation if the view is losing focus.
+        if (!focused) {
+            performValidation();
+        }
+        if (!focused && !mPopup.isDropDownAlwaysVisible()) {
+            dismissDropDown();
+        }
+
+        mController.resumeCursorMovementHandlingAndApplyChanges();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        dismissDropDown();
+        super.onDetachedFromWindow();
+    }
+
+    /**
+     * <p>Closes the drop down if present on screen.</p>
+     */
+    protected void dismissDropDown() {
+        InputMethodManager imm = InputMethodManager.peekInstance();
+        if (imm != null) {
+            imm.displayCompletions(this, null);
+        }
+        mPopup.dismiss();
+        mPopupCanBeUpdated = false;
+    }
+
+    @Override
+    protected boolean setFrame(final int l, int t, final int r, int b) {
+        boolean result = super.setFrame(l, t, r, b);
+
+        if (isPopupShowing()) {
+            showDropDown();
+        }
+
+        return result;
+    }
+
+    /**
+     * Ensures that the drop down is not obscuring the IME.
+     * @param visible whether the ime should be in front. If false, the ime is pushed to
+     * the background.
+     * @hide internal used only here and SearchDialog
+     */
+    private void ensureImeVisible(boolean visible) {
+        mPopup.setInputMethodMode(visible
+                ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+        showDropDown();
+    }
+
+    /**
+     * <p>Displays the drop down on screen.</p>
+     */
+    protected void showDropDown() {
+        if (mPopup.getAnchorView() == null) {
+            if (mDropDownAnchorId != View.NO_ID) {
+                mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId));
+            } else {
+                mPopup.setAnchorView(this);
+            }
+        }
+        if (!isPopupShowing()) {
+            // Make sure the list does not obscure the IME when shown for the first time.
+            mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED);
+        }
+        mPopup.show();
+    }
+
+    private void buildImeCompletions() {
+        final ListAdapter adapter = mAdapter;
+        if (adapter != null) {
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null) {
+                final int count = Math.min(adapter.getCount(), 20);
+                CompletionInfo[] completions = new CompletionInfo[count];
+                int realCount = 0;
+
+                for (int i = 0; i < count; i++) {
+                    if (adapter.isEnabled(i)) {
+                        realCount++;
+                        Object item = adapter.getItem(i);
+                        long id = adapter.getItemId(i);
+                        completions[i] = new CompletionInfo(id, i,
+                                convertSelectionToString(item));
+                    }
+                }
+
+                if (realCount != count) {
+                    CompletionInfo[] tmp = new CompletionInfo[realCount];
+                    System.arraycopy(completions, 0, tmp, 0, realCount);
+                    completions = tmp;
+                }
+
+                imm.displayCompletions(this, completions);
+            }
+        }
+    }
+
+    private void performValidation() {
+    }
+
+    /**
+     * Returns the Filter obtained from {@link Filterable#getFilter},
+     * or <code>null</code> if {@link #setAdapter} was not called with
+     * a Filterable.
+     */
+    protected Filter getFilter() {
+        return mFilter;
+    }
+
+    private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
+        @Override
+        public void onItemClick(AdapterView parent, View v, int position, long id) {
+            performCompletion(v, position, id);
+        }
+    }
+
+    /**
+     * Allows us a private hook into the on click event without preventing users from setting
+     * their own click listener.
+     */
+    private class PassThroughClickListener implements OnClickListener {
+
+        private View.OnClickListener mWrapped;
+
+        /** {@inheritDoc} */
+        @Override
+        public void onClick(View v) {
+            onClickImpl();
+
+            if (mWrapped != null) mWrapped.onClick(v);
+        }
+    }
+
+    private class PopupDataSetObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            if (mAdapter != null) {
+                // If the popup is not showing already, showing it will cause
+                // the list of data set observers attached to the adapter to
+                // change. We can't do it from here, because we are in the middle
+                // of iterating through the list of observers.
+                post(new Runnable() {
+                    @Override
+                    public void run() {
+                        final SuggestionsAdapter adapter = mAdapter;
+                        if (adapter != null) {
+                            // This will re-layout, thus resetting mDataChanged, so that the
+                            // listView click listener stays responsive
+                            updateDropDownForFilter(adapter.getCount());
+                        }
+
+                        updateText(adapter);
+                    }
+                });
+            }
+        }
+    }
+
+    private void updateText(SuggestionsAdapter adapter) {
+        // FIXME: Turn this on only when instant is being used.
+        // 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);
+        }
+    }
+
+    @Override
+    public void setText(CharSequence text, BufferType type) {
+        Editable buffer = getEditableText();
+        if (text == null) text = "";
+        // if we already have a buffer, we must not replace it with a new one as this will break
+        // mController. Write the new text into the existing buffer instead.
+        if (buffer == null) {
+            super.setText(text, type);
+        } else {
+            buffer.replace(0, buffer.length(), text);
+        }
+    }
+
+    public void setText(CharSequence text, boolean filter) {
+        if (filter) {
+            setText(text);
+        } else {
+            mBlockCompletion = true;
+            setText(text);
+            mBlockCompletion = false;
+        }
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        if (superState instanceof TextView.SavedState) {
+            // get rid of TextView's saved state, we override it.
+            superState = ((TextView.SavedState) superState).getSuperState();
+        }
+        if (superState == null) {
+            superState = AbsSavedState.EMPTY_STATE;
+        }
+        return mController.saveInstanceState(superState);
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        super.onRestoreInstanceState(mController.restoreInstanceState(state));
+    }
+
+    public void addQueryTextWatcher(final SuggestedTextController.TextChangeWatcher watcher) {
+        mController.addUserTextChangeWatcher(watcher);
+    }
+
+    public void setSuggestedText(String text) {
+        mController.setSuggestedText(text);
+    }
+}
diff --git a/src/com/android/browser/search/SearchEngineInfo.java b/src/com/android/browser/search/SearchEngineInfo.java
index 6f0b1d5..af6fa70 100644
--- a/src/com/android/browser/search/SearchEngineInfo.java
+++ b/src/com/android/browser/search/SearchEngineInfo.java
@@ -17,6 +17,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -58,9 +59,12 @@
      */
     public SearchEngineInfo(Context context, String name) throws IllegalArgumentException {
         mName = name;
-
         Resources res = context.getResources();
+
         int id_data = res.getIdentifier(name, "array", context.getPackageName());
+        if (id_data == 0) {
+            throw new IllegalArgumentException("No resources found for " + name);
+        }
         mSearchEngineData = res.getStringArray(id_data);
 
         if (mSearchEngineData == null) {