summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNarayan Kamath <narayan@google.com>2011-02-23 12:01:13 +0000
committerNarayan Kamath <narayan@google.com>2011-03-02 18:10:21 +0000
commit80aad8d851601d39f73214c198111ca49e25f654 (patch)
treef0f859bcd85f8f203243404165034038637e75a5
parent2eb1190bdda1d2d53c049aa3f5000af6a150e2ca (diff)
downloadandroid_packages_apps_Gello-80aad8d851601d39f73214c198111ca49e25f654.tar.gz
android_packages_apps_Gello-80aad8d851601d39f73214c198111ca49e25f654.tar.bz2
android_packages_apps_Gello-80aad8d851601d39f73214c198111ca49e25f654.zip
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
-rw-r--r--src/com/android/browser/SuggestionsAdapter.java10
-rw-r--r--src/com/android/browser/TitleBarXLarge.java19
-rw-r--r--src/com/android/browser/UrlInputView.java7
-rw-r--r--src/com/android/browser/autocomplete/SuggestedSpan.java34
-rw-r--r--src/com/android/browser/autocomplete/SuggestedTextController.java508
-rw-r--r--src/com/android/browser/autocomplete/SuggestiveAutoCompleteTextView.java828
-rw-r--r--src/com/android/browser/search/SearchEngineInfo.java6
-rw-r--r--tests/src/com/android/browser/autocomplete/SuggestedTextControllerTest.java547
8 files changed, 1936 insertions, 23 deletions
diff --git a/src/com/android/browser/SuggestionsAdapter.java b/src/com/android/browser/SuggestionsAdapter.java
index 6e555396..3636bbf0 100644
--- a/src/com/android/browser/SuggestionsAdapter.java
+++ b/src/com/android/browser/SuggestionsAdapter.java
@@ -392,11 +392,11 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable,
/**
* 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 85935a0e..1b33084e 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.Context;
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 @@ import java.util.List;
* 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 @@ public class TitleBarXLarge extends TitleBarBase
mUrlInput.setController(mUiController);
mUrlInput.setOnFocusChangeListener(this);
mUrlInput.setSelectAllOnFocus(true);
- mUrlInput.addTextChangedListener(this);
+ mUrlInput.addQueryTextWatcher(this);
setFocusState(false);
}
@@ -372,7 +369,7 @@ public class TitleBarXLarge extends TitleBarBase
// 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 @@ public class TitleBarXLarge extends TitleBarBase
}
}
- @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 2ec21111..362e941b 100644
--- a/src/com/android/browser/UrlInputView.java
+++ b/src/com/android/browser/UrlInputView.java
@@ -18,6 +18,7 @@ package com.android.browser;
import com.android.browser.SuggestionsAdapter.CompletionListener;
import com.android.browser.SuggestionsAdapter.SuggestItem;
+import com.android.browser.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.View;
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 @@ import java.util.List;
* url/search input view
* handling suggestions
*/
-public class UrlInputView extends AutoCompleteTextView
+public class UrlInputView extends SuggestiveAutoCompleteTextView
implements OnEditorActionListener,
CompletionListener, OnItemClickListener {
@@ -261,4 +261,7 @@ public class UrlInputView extends AutoCompleteTextView
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 00000000..dc04cb2f
--- /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 00000000..e9b74cea
--- /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 00000000..d93066c1
--- /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 6f0b1d5b..af6fa706 100644
--- a/src/com/android/browser/search/SearchEngineInfo.java
+++ b/src/com/android/browser/search/SearchEngineInfo.java
@@ -17,6 +17,7 @@ package com.android.browser.search;
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 class SearchEngineInfo {
*/
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) {
diff --git a/tests/src/com/android/browser/autocomplete/SuggestedTextControllerTest.java b/tests/src/com/android/browser/autocomplete/SuggestedTextControllerTest.java
new file mode 100644
index 00000000..f162e3b0
--- /dev/null
+++ b/tests/src/com/android/browser/autocomplete/SuggestedTextControllerTest.java
@@ -0,0 +1,547 @@
+/*
+ * 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.autocomplete.SuggestedTextController.TextOwner;
+
+import android.graphics.Color;
+import android.os.Parcelable;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextWatcher;
+import android.view.AbsSavedState;
+
+/**
+ * Test cases for {@link SuggestedTextController}.
+ */
+@SmallTest
+public class SuggestedTextControllerTest extends AndroidTestCase {
+
+ // these two must have a common prefix (but not be identical):
+ private static final String RUBY_MURRAY = "ruby murray";
+ private static final String RUBY_TUESDAY = "ruby tuesday";
+ private static final String EXTRA_USER_TEXT = " curry";
+ // no common prefix with the top two above:
+ private static final String TOD_SLOAN = "tod sloan";
+
+ private SuggestedTextController mController;
+ private SpannableStringBuilder mString;
+
+ private SuggestedTextController m2ndController;
+ private SpannableStringBuilder m2ndString;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mString = new SpannableStringBuilder();
+ Selection.setSelection(mString, 0); // position cursor
+ mController = new SuggestedTextController(new BufferTextOwner(mString), Color.GRAY);
+ checkInvariant();
+ }
+
+ private void create2ndController() {
+ m2ndString = new SpannableStringBuilder();
+ Selection.setSelection(m2ndString, 0); // position cursor
+ m2ndController = new SuggestedTextController(new BufferTextOwner(m2ndString), Color.GRAY);
+ check2ndInvariant();
+ }
+
+ private int cursorPos(Spannable string) {
+ int selStart = Selection.getSelectionStart(string);
+ int selEnd = Selection.getSelectionEnd(string);
+ assertEquals("Selection has non-zero length", selStart, selEnd);
+ return selEnd;
+ }
+
+ private int cursorPos() {
+ return cursorPos(mString);
+ }
+
+ private void insertAtCursor(String text) {
+ mString.insert(cursorPos(), text);
+ checkInvariant();
+ }
+
+ private void insertAtCursor(char ch) {
+ insertAtCursor(Character.toString(ch));
+ }
+
+ private void insertAt2ndCursor(String text) {
+ m2ndString.insert(cursorPos(m2ndString), text);
+ check2ndInvariant();
+ }
+
+ private void insertAt2ndCursor(char ch) {
+ insertAt2ndCursor(Character.toString(ch));
+ }
+
+ private void deleteBeforeCursor(int count) {
+ int pos = cursorPos();
+ count = Math.min(pos, count);
+ mString.delete(pos - count, pos);
+ checkInvariant();
+ }
+
+ private void replaceSelection(String withThis) {
+ mString.replace(Selection.getSelectionStart(mString),
+ Selection.getSelectionEnd(mString), withThis);
+ checkInvariant();
+ }
+
+ private void setSuggested(String suggested) {
+ mController.setSuggestedText(suggested);
+ checkInvariant();
+ }
+
+ private void set2ndSuggested(String suggested) {
+ m2ndController.setSuggestedText(suggested);
+ check2ndInvariant();
+ }
+
+ private void checkInvariant() {
+ mController.checkInvariant(mString);
+ }
+
+ private void check2ndInvariant() {
+ m2ndController.checkInvariant(m2ndString);
+ }
+
+ private void assertUserEntered(String expected, SuggestedTextController controller) {
+ assertEquals("User entered text not as expected", expected, controller.getUserText());
+ }
+
+ private void assertUserEntered(String expected) {
+ assertUserEntered(expected, mController);
+ }
+
+ private void assertBuffer(String expected, Editable string) {
+ assertEquals("Buffer contents not as expected", expected, string.toString());
+ }
+
+ private void assertBuffer(String expected) {
+ assertBuffer(expected, mString);
+ }
+
+ private void assertCursorPos(int where, Spannable string) {
+ assertEquals("Cursor not at expected position", where, cursorPos(string));
+ }
+
+ private void assertCursorPos(int where) {
+ assertCursorPos(where, mString);
+ }
+
+ private static final String commonPrefix(String a, String b) {
+ int pos = 0;
+ while (a.charAt(pos) == b.charAt(pos)) {
+ pos++;
+ }
+ assertTrue("No common prefix between '" + a + "' and '" + b + "'", pos > 0);
+ return a.substring(0, pos);
+ }
+
+ public void testTypeNoSuggested() {
+ for (int i = 0; i < RUBY_MURRAY.length(); ++i) {
+ assertCursorPos(i);
+ assertUserEntered(RUBY_MURRAY.substring(0, i));
+ assertBuffer(RUBY_MURRAY.substring(0, i));
+ insertAtCursor(RUBY_MURRAY.substring(i, i + 1));
+ }
+ }
+
+ public void testTypeSuggested() {
+ setSuggested(RUBY_MURRAY);
+ assertCursorPos(0);
+ assertBuffer(RUBY_MURRAY);
+ for (int i = 0; i < RUBY_MURRAY.length(); ++i) {
+ assertCursorPos(i);
+ assertUserEntered(RUBY_MURRAY.substring(0, i));
+ assertBuffer(RUBY_MURRAY);
+ insertAtCursor(RUBY_MURRAY.charAt(i));
+ }
+ }
+
+ public void testSetSuggestedAfterTextEntry() {
+ final int count = RUBY_MURRAY.length() / 2;
+ for (int i = 0; i < count; ++i) {
+ assertCursorPos(i);
+ assertUserEntered(RUBY_MURRAY.substring(0, i));
+ insertAtCursor(RUBY_MURRAY.substring(i, i + 1));
+ }
+ setSuggested(RUBY_MURRAY);
+ assertUserEntered(RUBY_MURRAY.substring(0, count));
+ assertBuffer(RUBY_MURRAY);
+ }
+
+ public void testTypeSuggestedUpperCase() {
+ setSuggested(RUBY_MURRAY);
+ assertCursorPos(0);
+ for (int i = 0; i < RUBY_MURRAY.length(); ++i) {
+ assertCursorPos(i);
+ assertUserEntered(RUBY_MURRAY.substring(0, i).toUpperCase());
+ assertTrue("Buffer doesn't contain suggested text",
+ RUBY_MURRAY.equalsIgnoreCase(mString.toString()));
+ insertAtCursor(Character.toUpperCase(RUBY_MURRAY.charAt(i)));
+ }
+ }
+
+ public void testChangeSuggestedText() {
+ String pref = commonPrefix(RUBY_MURRAY, RUBY_TUESDAY);
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(pref);
+ assertBuffer(RUBY_MURRAY);
+ assertUserEntered(pref);
+ setSuggested(RUBY_TUESDAY);
+ assertBuffer(RUBY_TUESDAY);
+ assertUserEntered(pref);
+ }
+
+ public void testTypeNonSuggested() {
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(RUBY_MURRAY.charAt(0));
+ assertBuffer(RUBY_MURRAY);
+ insertAtCursor('x');
+ assertBuffer("rx");
+ }
+
+ public void testTypeNonSuggestedThenNewSuggestion() {
+ final String pref = commonPrefix(RUBY_MURRAY, RUBY_TUESDAY);
+ setSuggested(RUBY_MURRAY);
+ assertCursorPos(0);
+ insertAtCursor(pref);
+ assertCursorPos(pref.length());
+ assertUserEntered(pref);
+ insertAtCursor(RUBY_TUESDAY.charAt(pref.length()));
+ assertBuffer(RUBY_TUESDAY.substring(0, pref.length() + 1));
+ setSuggested(RUBY_TUESDAY);
+ assertBuffer(RUBY_TUESDAY);
+ }
+
+ public void testChangeSuggestedToNonUserEntered() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(half);
+ setSuggested(TOD_SLOAN);
+ assertUserEntered(half);
+ assertBuffer(half);
+ }
+
+ public void testChangeSuggestedToUserEntered() {
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(TOD_SLOAN);
+ setSuggested(TOD_SLOAN);
+ assertUserEntered(TOD_SLOAN);
+ assertBuffer(TOD_SLOAN);
+ }
+
+ public void testChangeSuggestedToEmpty() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(half);
+ setSuggested(null);
+ assertUserEntered(half);
+ assertBuffer(half);
+ }
+
+ public void testChangeSuggestedToEmptyFromUserEntered() {
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(RUBY_MURRAY);
+ setSuggested(null);
+ assertUserEntered(RUBY_MURRAY);
+ assertBuffer(RUBY_MURRAY);
+ }
+
+ public void typeNonSuggestedThenDelete() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ assertCursorPos(0);
+ insertAtCursor(half);
+ assertCursorPos(half.length());
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor('x');
+ assertBuffer(half + "x");
+ deleteBeforeCursor(1);
+ assertUserEntered(half);
+ assertBuffer(RUBY_MURRAY);
+ }
+
+ public void testDeleteMultipleFromSuggested() {
+ final String twoThirds = RUBY_MURRAY.substring(0, (RUBY_MURRAY.length() * 2) / 3);
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(twoThirds);
+ assertCursorPos(twoThirds.length());
+ // select some of the text just entered:
+ Selection.setSelection(mString, RUBY_MURRAY.length() / 3, twoThirds.length());
+ // and delete it:
+ replaceSelection("");
+ assertCursorPos(RUBY_MURRAY.length() / 3);
+ assertUserEntered(RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 3));
+ assertBuffer(RUBY_MURRAY);
+ }
+
+ public void testDeleteMultipleToFormSuggested() {
+ final String pref = commonPrefix(RUBY_TUESDAY, RUBY_MURRAY);
+ final int extra = (RUBY_TUESDAY.length() - pref.length()) / 2;
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(RUBY_TUESDAY.substring(0, pref.length() + extra));
+ assertCursorPos(pref.length() + extra);
+ // select and delete extra characters, leaving just prefix
+ Selection.setSelection(mString, pref.length(), pref.length() + extra);
+ replaceSelection("");
+ assertCursorPos(pref.length());
+ assertBuffer(RUBY_MURRAY);
+ assertUserEntered(pref);
+ }
+
+ public void testBackspaceWithinUserTextFromSuggested() {
+ StringBuffer half = new StringBuffer(RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2));
+ insertAtCursor(half.toString());
+ int backSpaceFrom = half.length() / 2;
+ Selection.setSelection(mString, backSpaceFrom);
+ deleteBeforeCursor(1);
+ assertCursorPos(backSpaceFrom - 1);
+ half.delete(backSpaceFrom - 1, backSpaceFrom);
+ assertUserEntered(half.toString());
+ assertBuffer(half.toString());
+ }
+
+ public void testInsertWithinUserTextToFormSuggested() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ StringBuffer initial = new StringBuffer(half);
+ int pos = initial.length() / 2;
+ char toInsert = initial.charAt(pos);
+ initial.delete(pos, pos + 1);
+ insertAtCursor(initial.toString());
+ setSuggested(RUBY_MURRAY);
+ assertUserEntered(initial.toString());
+ assertBuffer(initial.toString());
+ Selection.setSelection(mString, pos);
+ insertAtCursor(toInsert);
+ assertCursorPos(pos + 1);
+ assertUserEntered(half);
+ assertBuffer(RUBY_MURRAY);
+ }
+
+ public void testEnterTextBeyondSuggested() {
+ setSuggested(RUBY_MURRAY);
+ int i = RUBY_MURRAY.length() / 2;
+ insertAtCursor(RUBY_MURRAY.substring(0, i));
+ String query = RUBY_MURRAY + EXTRA_USER_TEXT;
+ for (; i < query.length(); ++i) {
+ assertUserEntered(query.substring(0, i));
+ if (i <= RUBY_MURRAY.length()) {
+ assertBuffer(RUBY_MURRAY);
+ }
+ insertAtCursor(query.charAt(i));
+ }
+ assertUserEntered(query);
+ }
+
+ public void testDeleteFromLongerThanSuggested() {
+ setSuggested(RUBY_MURRAY);
+ final String entered = RUBY_MURRAY + EXTRA_USER_TEXT;
+ insertAtCursor(entered);
+ for (int i = entered.length(); i > (RUBY_MURRAY.length() / 2); --i) {
+ assertCursorPos(i);
+ assertUserEntered(entered.substring(0, i));
+ if (i <= RUBY_MURRAY.length()) {
+ assertBuffer(RUBY_MURRAY);
+ }
+ deleteBeforeCursor(1);
+ }
+ }
+
+ public void testReplaceWithShorterToFormSuggested() {
+ final String pref = commonPrefix(RUBY_TUESDAY, RUBY_MURRAY);
+ final int extra = (RUBY_TUESDAY.length() - pref.length()) / 2;
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(RUBY_TUESDAY.substring(0, pref.length() + extra));
+ assertCursorPos(pref.length() + extra);
+ // select and replace extra characters, to match suggested
+ Selection.setSelection(mString, pref.length(), pref.length() + extra);
+ replaceSelection(RUBY_MURRAY.substring(pref.length(), pref.length() + extra - 1));
+ assertBuffer(RUBY_MURRAY);
+ assertUserEntered(RUBY_MURRAY.substring(0, pref.length() + extra - 1));
+ }
+
+ public void testReplaceWithSameLengthToFormSuggested() {
+ final String pref = commonPrefix(RUBY_TUESDAY, RUBY_MURRAY);
+ final int extra = (RUBY_TUESDAY.length() - pref.length()) / 2;
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(RUBY_TUESDAY.substring(0, pref.length() + extra));
+ assertCursorPos(pref.length() + extra);
+ // select and replace extra characters, to match suggested
+ Selection.setSelection(mString, pref.length(), pref.length() + extra);
+ replaceSelection(RUBY_MURRAY.substring(pref.length(), pref.length() + extra));
+ assertBuffer(RUBY_MURRAY);
+ assertUserEntered(RUBY_MURRAY.substring(0, pref.length() + extra));
+ }
+
+ public void testReplaceWithLongerToFormSuggested() {
+ final String pref = commonPrefix(RUBY_TUESDAY, RUBY_MURRAY);
+ final int extra = (RUBY_TUESDAY.length() - pref.length()) / 2;
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(RUBY_TUESDAY.substring(0, pref.length() + extra));
+ assertCursorPos(pref.length() + extra);
+ // select and replace extra characters, to match suggested
+ Selection.setSelection(mString, pref.length(), pref.length() + extra);
+ replaceSelection(RUBY_MURRAY.substring(pref.length(), pref.length() + extra + 1));
+ assertBuffer(RUBY_MURRAY);
+ assertUserEntered(RUBY_MURRAY.substring(0, pref.length() + extra + 1));
+ }
+
+ public void testMoveCursorIntoSuggested() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ insertAtCursor(half);
+ setSuggested(RUBY_MURRAY);
+ assertCursorPos(half.length());
+ Selection.setSelection(mString, half.length() + 1);
+ checkInvariant();
+ assertUserEntered(RUBY_MURRAY);
+ }
+
+ public void testMoveCursorWithinUserEntered() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ insertAtCursor(half);
+ setSuggested(RUBY_MURRAY);
+ assertCursorPos(half.length());
+ Selection.setSelection(mString, half.length() - 1);
+ checkInvariant();
+ assertUserEntered(half);
+ }
+
+ public void testSelectWithinSuggested() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ insertAtCursor(half);
+ setSuggested(RUBY_MURRAY);
+ assertCursorPos(half.length());
+ Selection.setSelection(mString, half.length() + 1, half.length() + 2);
+ checkInvariant();
+ assertUserEntered(RUBY_MURRAY);
+ }
+
+ public void testSelectStraddlingSuggested() {
+ final String half = RUBY_MURRAY.substring(0, RUBY_MURRAY.length() / 2);
+ insertAtCursor(half);
+ setSuggested(RUBY_MURRAY);
+ assertCursorPos(half.length());
+ Selection.setSelection(mString, half.length() - 1, half.length() + 1);
+ checkInvariant();
+ assertUserEntered(RUBY_MURRAY);
+ }
+
+ public void testSaveAndRestoreNoText() {
+ create2ndController();
+ Parcelable state = mController.saveInstanceState(AbsSavedState.EMPTY_STATE);
+ m2ndController.restoreInstanceState(state);
+ check2ndInvariant();
+ assertBuffer("", m2ndString);
+ }
+
+ public void testSaveAndRestoreWithSuggestedText() {
+ create2ndController();
+ setSuggested(TOD_SLOAN);
+ Parcelable state = mController.saveInstanceState(AbsSavedState.EMPTY_STATE);
+ m2ndController.restoreInstanceState(state);
+ check2ndInvariant();
+ assertBuffer(TOD_SLOAN, m2ndString);
+ assertUserEntered("", m2ndController);
+ }
+
+ public void testSaveAndRestoreWithUserEnteredAndSuggestedText() {
+ final String half = TOD_SLOAN.substring(0, TOD_SLOAN.length() / 2);
+ create2ndController();
+ setSuggested(TOD_SLOAN);
+ insertAtCursor(half);
+ Parcelable state = mController.saveInstanceState(AbsSavedState.EMPTY_STATE);
+ m2ndController.restoreInstanceState(state);
+ check2ndInvariant();
+ assertBuffer(TOD_SLOAN, m2ndString);
+ assertUserEntered(half, m2ndController);
+ assertCursorPos(half.length(), m2ndString);
+ }
+
+ public void testSaveAndRestoreWithNonSuggested() {
+ final String half = TOD_SLOAN.substring(0, TOD_SLOAN.length() / 2);
+ create2ndController();
+ setSuggested(RUBY_MURRAY);
+ insertAtCursor(half);
+ Parcelable state = mController.saveInstanceState(AbsSavedState.EMPTY_STATE);
+ m2ndController.restoreInstanceState(state);
+ check2ndInvariant();
+ assertBuffer(half, m2ndString);
+ assertUserEntered(half, m2ndController);
+ assertCursorPos(half.length(), m2ndString);
+ }
+
+ public void testSaveAndRestoreThenTypeSuggested() {
+ final String half = TOD_SLOAN.substring(0, TOD_SLOAN.length() / 2);
+ create2ndController();
+ set2ndSuggested(TOD_SLOAN);
+ insertAt2ndCursor(half);
+ insertAt2ndCursor('x');
+ Parcelable state = m2ndController.saveInstanceState(AbsSavedState.EMPTY_STATE);
+ mController.restoreInstanceState(state);
+ assertCursorPos(half.length() + 1);
+ // delete the x
+ deleteBeforeCursor(1);
+ assertCursorPos(half.length());
+ assertBuffer(TOD_SLOAN);
+ assertUserEntered(half);
+ }
+
+ public void testSuspendAndResumeCursorProcessing() {
+ final String half = TOD_SLOAN.substring(0, TOD_SLOAN.length() / 2);
+ setSuggested(TOD_SLOAN);
+ insertAtCursor(half);
+ mController.suspendCursorMovementHandling();
+ Selection.setSelection(mString, TOD_SLOAN.length());
+ Selection.setSelection(mString, half.length());
+ mController.resumeCursorMovementHandlingAndApplyChanges();
+ assertCursorPos(half.length());
+ assertUserEntered(half);
+ assertBuffer(TOD_SLOAN);
+ }
+
+ private static class BufferTextOwner implements TextOwner {
+
+ private final Editable mBuffer;
+
+ public BufferTextOwner(Editable buffer) {
+ mBuffer = buffer;
+ }
+
+ public void addTextChangedListener(TextWatcher watcher) {
+ mBuffer.setSpan(watcher , 0, mBuffer.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ public void removeTextChangedListener(TextWatcher watcher) {
+ mBuffer.removeSpan(watcher);
+ }
+
+ public Editable getText() {
+ return mBuffer;
+ }
+
+ public void setText(String text) {
+ mBuffer.replace(0, mBuffer.length(), text);
+ }
+
+ }
+
+}