diff --git a/mobile/android/base/AwesomeBar.java b/mobile/android/base/AwesomeBar.java index ecebc736e61..79f5ef3b346 100644 --- a/mobile/android/base/AwesomeBar.java +++ b/mobile/android/base/AwesomeBar.java @@ -46,7 +46,13 @@ import android.widget.Toast; import java.net.URLEncoder; -public class AwesomeBar extends GeckoActivity { +interface AutocompleteHandler { + void onAutocomplete(String res); +} + +public class AwesomeBar extends GeckoActivity + implements AutocompleteHandler, + TextWatcher { private static final String LOGTAG = "GeckoAwesomeBar"; public static final String URL_KEY = "url"; @@ -65,6 +71,10 @@ public class AwesomeBar extends GeckoActivity { private ContextMenuSubject mContextMenuSubject; private boolean mIsUsingGestureKeyboard; private boolean mDelayRestartInput; + // The previous autocomplete result returned to us + private String mAutoCompleteResult = ""; + // The user typed part of the autocomplete result + private String mAutoCompletePrefix = null; @Override public void onCreate(Bundle savedInstanceState) { @@ -168,35 +178,7 @@ public class AwesomeBar extends GeckoActivity { } }); - mText.addTextChangedListener(new TextWatcher() { - @Override - public void afterTextChanged(Editable s) { - String text = s.toString(); - mAwesomeTabs.filter(text); - - // If the AwesomeBar has a composition string, don't call updateGoButton(). - // That method resets IME and composition state will be broken. - if (!hasCompositionString(s)) { - updateGoButton(text); - } - - if (Build.VERSION.SDK_INT >= 11) { - getActionBar().hide(); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - // do nothing - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, - int count) { - // do nothing - } - }); + mText.addTextChangedListener(this); mText.setOnKeyListener(new View.OnKeyListener() { @Override @@ -727,4 +709,75 @@ public class AwesomeBar extends GeckoActivity { } return false; } + + // return early if we're backspacing through the string, or have no autocomplete results + public void onAutocomplete(final String result) { + final String text = mText.getText().toString(); + + if (result == null) { + mAutoCompleteResult = ""; + return; + } + + if (!result.startsWith(text) || text.equals(result)) { + return; + } + + mAutoCompleteResult = result; + mText.getText().append(result.substring(text.length())); + mText.setSelection(text.length(), result.length()); + } + + @Override + public void afterTextChanged(final Editable s) { + final String text = s.toString(); + boolean useHandler = false; + boolean reuseAutocomplete = false; + if (!hasCompositionString(s) && !StringUtils.isSearchQuery(text, false)) { + useHandler = true; + + // If you're hitting backspace (the string is getting smaller + // or is unchanged), don't autocomplete. + if (mAutoCompletePrefix != null && (mAutoCompletePrefix.length() >= text.length())) { + useHandler = false; + } else if (mAutoCompleteResult != null && mAutoCompleteResult.startsWith(text)) { + // If this text already matches our autocomplete text, autocomplete likely + // won't change. Just reuse the old autocomplete value. + useHandler = false; + reuseAutocomplete = true; + } + } + + // If this is the autocomplete text being set, don't run the filter. + if (mAutoCompleteResult == null || !mAutoCompleteResult.equals(text)) { + mAwesomeTabs.filter(text, useHandler ? this : null); + mAutoCompletePrefix = text; + + if (reuseAutocomplete) { + onAutocomplete(mAutoCompleteResult); + } + } + + // If the AwesomeBar has a composition string, don't call updateGoButton(). + // That method resets IME and composition state will be broken. + if (!hasCompositionString(s)) { + updateGoButton(text); + } + + if (Build.VERSION.SDK_INT >= 11) { + getActionBar().hide(); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + // do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + // do nothing + } } diff --git a/mobile/android/base/AwesomeBarTabs.java b/mobile/android/base/AwesomeBarTabs.java index 4dd8a7bcae7..a7b6d1b7dbb 100644 --- a/mobile/android/base/AwesomeBarTabs.java +++ b/mobile/android/base/AwesomeBarTabs.java @@ -173,7 +173,7 @@ public class AwesomeBarTabs extends TabHost } // Initialize "All Pages" list with no filter - filter(""); + filter("", null); } @Override @@ -308,7 +308,7 @@ public class AwesomeBarTabs extends TabHost return (HistoryTab)getAwesomeBarTabForTag("history"); } - public void filter(String searchTerm) { + public void filter(String searchTerm, AutocompleteHandler handler) { // If searching, disable left / right tab swipes mSearching = searchTerm.length() != 0; @@ -322,7 +322,7 @@ public class AwesomeBarTabs extends TabHost styleSelectedTab(); // Perform the actual search - allPages.filter(searchTerm); + allPages.filter(searchTerm, handler); // If searching, hide the tabs bar findViewById(R.id.tab_widget_container).setVisibility(mSearching ? View.GONE : View.VISIBLE); diff --git a/mobile/android/base/awesomebar/AllPagesTab.java b/mobile/android/base/awesomebar/AllPagesTab.java index 3e49fb01c48..c0ac2061e2a 100644 --- a/mobile/android/base/awesomebar/AllPagesTab.java +++ b/mobile/android/base/awesomebar/AllPagesTab.java @@ -25,6 +25,7 @@ import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Message; @@ -63,6 +64,8 @@ public class AllPagesTab extends AwesomeBarTab implements GeckoEventListener { private static final int SUGGESTION_TIMEOUT = 3000; private static final int SUGGESTION_MAX = 3; private static final int ANIMATION_DURATION = 250; + // The maximum number of rows deep in a search we'll dig for an autocomplete result + private static final int MAX_AUTOCOMPLETE_SEARCH = 20; private String mSearchTerm; private ArrayList mSearchEngines; @@ -76,6 +79,7 @@ public class AllPagesTab extends AwesomeBarTab implements GeckoEventListener { private View mSuggestionsOptInPrompt; private Handler mHandler; private ListView mListView; + private volatile AutocompleteHandler mAutocompleteHandler = null; private static final int MESSAGE_LOAD_FAVICONS = 1; private static final int MESSAGE_UPDATE_FAVICONS = 2; @@ -169,7 +173,9 @@ public class AllPagesTab extends AwesomeBarTab implements GeckoEventListener { } } - public void filter(String searchTerm) { + public void filter(String searchTerm, AutocompleteHandler handler) { + mAutocompleteHandler = handler; + AwesomeBarCursorAdapter adapter = getCursorAdapter(); adapter.filter(searchTerm); @@ -182,6 +188,61 @@ public class AllPagesTab extends AwesomeBarTab implements GeckoEventListener { } } + private void findAutocompleteFor(String searchTerm, Cursor cursor) { + if (TextUtils.isEmpty(searchTerm) || cursor == null || mAutocompleteHandler == null) + return; + + // avoid searching the path if we don't have to. Currently just decided by if there is + // a '/' character in the string + final String res = searchHosts(searchTerm, cursor, searchTerm.indexOf("/") > 0); + + if (res != null) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + // Its possible that mAutocompleteHandler has been destroyed + if (mAutocompleteHandler != null) { + mAutocompleteHandler.onAutocomplete(res); + mAutocompleteHandler = null; + } + } + }); + } + } + + private String searchHosts(String searchTerm, Cursor cursor, boolean searchPath) { + int i = 0; + if (cursor.moveToFirst()) { + int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL); + do { + final Uri url = Uri.parse(cursor.getString(urlIndex)); + String host = StringUtils.stripCommonSubdomains(url.getHost()); + // host may be null for about pages + if (host == null) + continue; + + StringBuilder hostBuilder = new StringBuilder(host); + if (hostBuilder.indexOf(searchTerm) == 0) { + return hostBuilder.append("/").toString(); + } + + if (searchPath) { + List path = url.getPathSegments(); + + for (String seg : path) { + hostBuilder.append("/").append(seg); + if (hostBuilder.indexOf(searchTerm) == 0) { + return hostBuilder.append("/").toString(); + } + } + } + + i++; + } while (i < MAX_AUTOCOMPLETE_SEARCH && cursor.moveToNext()); + } + return null; + } + /** * Query for suggestions, but don't show them yet. */ @@ -237,6 +298,8 @@ public class AllPagesTab extends AwesomeBarTab implements GeckoEventListener { Telemetry.HistogramAdd("FENNEC_AWESOMEBAR_ALLPAGES_EMPTY_TIME", time); mTelemetrySent = true; } + + findAutocompleteFor(constraint.toString(), c); return c; } }); diff --git a/mobile/android/base/util/StringUtils.java b/mobile/android/base/util/StringUtils.java index 96b412121e0..1c2b1f3aaa9 100644 --- a/mobile/android/base/util/StringUtils.java +++ b/mobile/android/base/util/StringUtils.java @@ -44,4 +44,26 @@ public class StringUtils { // Otherwise, text is ambiguous, and we keep its status unchanged return wasSearchQuery; } + + public static String stripScheme(String url) { + if (url == null) + return url; + + if (url.startsWith("http://")) { + return url.substring(7); + } + return url; + } + + public static String stripCommonSubdomains(String host) { + if (host == null) + return host; + // In contrast to desktop, we also strip mobile subdomains, + // since its unlikely users are intentionally typing them + if (host.startsWith("www.")) return host.substring(4); + else if (host.startsWith("mobile.")) return host.substring(7); + else if (host.startsWith("m.")) return host.substring(2); + return host; + } + }