author | Lucas Rocha <lucasr@mozilla.com> |
Tue, 11 Jun 2013 18:01:35 +0100 | |
changeset 143324 | ea79867c3ffda6170afe96fb8a8bdd2085548a84 |
parent 143323 | 6509a6fd9bd55f733e854848cfc6f92999ca4fde |
child 143325 | 478e2819dbf732bd594e3fb8c19e2a14234a3c6d |
push id | 25130 |
push user | lrocha@mozilla.com |
push date | Wed, 21 Aug 2013 09:41:27 +0000 |
treeherder | mozilla-central@b2486721572e [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | bnicholson |
bugs | 871650 |
milestone | 24.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -69,16 +69,18 @@ import java.net.URL; import java.util.EnumSet; import java.util.Vector; abstract public class BrowserApp extends GeckoApp implements TabsPanel.TabsLayoutChangeListener, PropertyAnimator.PropertyAnimationListener, View.OnKeyListener, GeckoLayerClient.OnMetricsChangedListener, + BrowserSearch.OnSearchListener, + BrowserSearch.OnEditSuggestionListener, BrowserSearch.OnUrlOpenListener, HomePager.OnUrlOpenListener { private static final String LOGTAG = "GeckoBrowserApp"; private static final String PREF_CHROME_DYNAMICTOOLBAR = "browser.chrome.dynamictoolbar"; private static final String ABOUT_HOME = "about:home"; @@ -1124,24 +1126,28 @@ abstract public class BrowserApp extends @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(STATE_DYNAMIC_TOOLBAR_ENABLED, mDynamicToolbarEnabled); } private void openUrl(String url) { + openUrl(url, null); + } + + private void openUrl(String url, String searchEngine) { mBrowserToolbar.setProgressVisibility(true); int flags = Tabs.LOADURL_NONE; if (mBrowserToolbar.getEditingTarget() == EditingTarget.NEW_TAB) { flags |= Tabs.LOADURL_NEW_TAB; } - Tabs.getInstance().loadUrl(url, flags); + Tabs.getInstance().loadUrl(url, searchEngine, -1, flags); hideBrowserSearch(); mBrowserToolbar.cancelEdit(); } /* Favicon methods */ private void loadFavicon(final Tab tab) { maybeCancelFaviconLoad(tab); @@ -1819,16 +1825,28 @@ abstract public class BrowserApp extends } // (HomePager|BrowserSearch).OnUrlOpenListener @Override public void onUrlOpen(String url) { openUrl(url); } + // BrowserSearch.OnSearchListener + @Override + public void onSearch(SearchEngine engine, String text) { + openUrl(text, engine.name); + } + + // BrowserSearch.OnEditSuggestionListener + @Override + public void onEditSuggestion(String suggestion) { + mBrowserToolbar.onEditSuggestion(suggestion); + } + @Override public int getLayout() { return R.layout.gecko_app; } @Override protected String getDefaultProfileName() { String profile = GeckoProfile.findDefaultProfile(this); return (profile != null ? profile : "default"); }
--- a/mobile/android/base/BrowserSearch.java +++ b/mobile/android/base/BrowserSearch.java @@ -4,97 +4,188 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; import org.mozilla.gecko.db.BrowserContract.Combined; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.BrowserDB.URLColumns; import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.home.TwoLinePageRow; +import org.mozilla.gecko.widget.FaviconView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.os.Bundle; import android.content.res.Configuration; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; +import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.text.TextUtils; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater; import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.SimpleCursorAdapter; +import android.widget.TextView; import java.util.ArrayList; /** * Fragment that displays frecency search results in a ListView. */ -public class BrowserSearch extends Fragment implements AdapterView.OnItemClickListener { +public class BrowserSearch extends Fragment implements AdapterView.OnItemClickListener, + GeckoEventListener { + // Logging tag name + private static final String LOGTAG = "GeckoBrowserSearch"; + // Cursor loader ID for search query private static final int SEARCH_LOADER_ID = 0; // Cursor loader ID for favicons query private static final int FAVICONS_LOADER_ID = 1; + // AsyncTask loader ID for suggestion query + private static final int SUGGESTION_LOADER_ID = 2; + // Argument containing list of urls for the favicons loader private static final String FAVICONS_LOADER_URLS_ARG = "urls"; + // Timeout for the suggestion client to respond + private static final int SUGGESTION_TIMEOUT = 3000; + + // Maximum number of results returned by the suggestion client + private static final int SUGGESTION_MAX = 3; + // Holds the current search term to use in the query private String mSearchTerm; // Adapter for the list of search results private SearchAdapter mAdapter; // The view shown by the fragment. private ListView mList; + // Client that performs search suggestion queries + private SuggestClient mSuggestClient; + + // List of search engines from gecko + private ArrayList<SearchEngine> mSearchEngines; + + // Whether search suggestions are enabled or not + private boolean mSuggestionsEnabled; + // Callbacks used for the search and favicon cursor loaders private CursorLoaderCallbacks mCursorLoaderCallbacks; + // Callbacks used for the search suggestion loader + private SuggestionLoaderCallbacks mSuggestionLoaderCallbacks; + + // Inflater used by the adapter + private LayoutInflater mInflater; + // On URL open listener private OnUrlOpenListener mUrlOpenListener; + // On search listener + private OnSearchListener mSearchListener; + + // On edit suggestion listener + private OnEditSuggestionListener mEditSuggestionListener; + public interface OnUrlOpenListener { public void onUrlOpen(String url); } + public interface OnSearchListener { + public void onSearch(SearchEngine engine, String text); + } + + public interface OnEditSuggestionListener { + public void onEditSuggestion(String suggestion); + } + public static BrowserSearch newInstance() { return new BrowserSearch(); } public BrowserSearch() { mSearchTerm = ""; } @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + registerEventListener("SearchEngines:Data"); + + // The search engines list is reused beyond the life-cycle of + // this fragment. + if (mSearchEngines == null) { + mSearchEngines = new ArrayList<SearchEngine>(); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null)); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + unregisterEventListener("SearchEngines:Data"); + } + + @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mUrlOpenListener = (OnUrlOpenListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement BrowserSearch.OnUrlOpenListener"); } + + try { + mSearchListener = (OnSearchListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement BrowserSearch.OnSearchListener"); + } + + try { + mEditSuggestionListener = (OnEditSuggestionListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement BrowserSearch.OnEditSuggestionListener"); + } + + mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public void onDetach() { super.onDetach(); mUrlOpenListener = null; + mSearchListener = null; + mEditSuggestionListener = null; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // All list views are styled to look the same with a global activity theme. // If the style of the list changes, inflate it from an XML. mList = new ListView(container.getContext()); return mList; @@ -110,22 +201,113 @@ public class BrowserSearch extends Fragm @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Intialize the search adapter mAdapter = new SearchAdapter(getActivity()); mList.setAdapter(mAdapter); + // Only create an instance when we need it + mSuggestionLoaderCallbacks = null; + + // Create callbacks before the initial loader is started mCursorLoaderCallbacks = new CursorLoaderCallbacks(); // Reconnect to the loader only if present getLoaderManager().initLoader(SEARCH_LOADER_ID, null, mCursorLoaderCallbacks); } + @Override + public void handleMessage(String event, final JSONObject message) { + if (event.equals("SearchEngines:Data")) { + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + setSearchEngines(message); + } + }); + } + } + + private void filterSuggestions() { + if (mSuggestClient == null || !mSuggestionsEnabled) { + return; + } + + if (mSuggestionLoaderCallbacks == null) { + mSuggestionLoaderCallbacks = new SuggestionLoaderCallbacks(); + } + + getLoaderManager().restartLoader(SUGGESTION_LOADER_ID, null, mSuggestionLoaderCallbacks); + } + + private void setSuggestions(ArrayList<String> suggestions) { + mSearchEngines.get(0).suggestions = suggestions; + mAdapter.notifyDataSetChanged(); + } + + private void setSearchEngines(JSONObject data) { + try { + final JSONObject suggest = data.getJSONObject("suggest"); + final String suggestEngine = suggest.optString("engine", null); + final String suggestTemplate = suggest.optString("template", null); + final boolean suggestionsPrompted = suggest.getBoolean("prompted"); + final JSONArray engines = data.getJSONArray("searchEngines"); + + mSuggestionsEnabled = suggest.getBoolean("enabled"); + + ArrayList<SearchEngine> searchEngines = new ArrayList<SearchEngine>(); + for (int i = 0; i < engines.length(); i++) { + final JSONObject engineJSON = engines.getJSONObject(i); + final String name = engineJSON.getString("name"); + final String identifier = engineJSON.getString("identifier"); + final String iconURI = engineJSON.getString("iconURI"); + final Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconURI); + + if (name.equals(suggestEngine) && suggestTemplate != null) { + // Suggest engine should be at the front of the list + searchEngines.add(0, new SearchEngine(name, identifier, icon)); + + // The only time Tabs.getInstance().getSelectedTab() should + // be null is when we're restoring after a crash. We should + // never restore private tabs when that happens, so it + // should be safe to assume that null means non-private. + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab == null || !tab.isPrivate()) { + mSuggestClient = new SuggestClient(getActivity(), suggestTemplate, + SUGGESTION_TIMEOUT, SUGGESTION_MAX); + } + } else { + searchEngines.add(new SearchEngine(name, identifier, icon)); + } + } + + mSearchEngines = searchEngines; + + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + + // FIXME: restore suggestion opt-in UI + } catch (JSONException e) { + Log.e(LOGTAG, "Error getting search engine JSON", e); + } + + filterSuggestions(); + } + + private void registerEventListener(String eventName) { + GeckoAppShell.getEventDispatcher().registerEventListener(eventName, this); + } + + private void unregisterEventListener(String eventName) { + GeckoAppShell.getEventDispatcher().unregisterEventListener(eventName, this); + } + private ArrayList<String> getUrlsWithoutFavicon(Cursor c) { ArrayList<String> urls = new ArrayList<String>(); if (c == null || !c.moveToFirst()) { return urls; } final Favicons favicons = Favicons.getInstance(); @@ -163,18 +345,23 @@ public class BrowserSearch extends Fragm if (TextUtils.equals(mSearchTerm, searchTerm)) { return; } mSearchTerm = searchTerm; if (isVisible()) { + // The adapter depends on the search term to determine its number + // of items. Make it we notify the view about it. + mAdapter.notifyDataSetChanged(); + + // Restart loaders with the new search term getLoaderManager().restartLoader(SEARCH_LOADER_ID, null, mCursorLoaderCallbacks); - + filterSuggestions(); } } private static class SearchCursorLoader extends SimpleCursorLoader { // Max number of search results private static final int SEARCH_LIMIT = 100; // The target search term associated with the loader @@ -235,40 +422,201 @@ public class BrowserSearch extends Fragm } favicon = favicons.scaleImage(favicon); favicons.putFaviconInMemCache(url, favicon); } while (c.moveToNext()); } } + private static class SuggestionAsyncLoader extends AsyncTaskLoader<ArrayList<String>> { + private final SuggestClient mSuggestClient; + private final String mSearchTerm; + private ArrayList<String> mSuggestions; + + public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) { + super(context); + mSuggestClient = suggestClient; + mSearchTerm = searchTerm; + mSuggestions = null; + } + + @Override + public ArrayList<String> loadInBackground() { + return mSuggestClient.query(mSearchTerm); + } + + @Override + public void deliverResult(ArrayList<String> suggestions) { + mSuggestions = suggestions; + + if (isStarted()) { + super.deliverResult(mSuggestions); + } + } + + @Override + protected void onStartLoading() { + if (mSuggestions != null) { + deliverResult(mSuggestions); + } + + if (takeContentChanged() || mSuggestions == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + + onStopLoading(); + mSuggestions = null; + } + } + + private class SearchEntryViewHolder { + public FlowLayout suggestionView; + public FaviconView iconView; + public LinearLayout userEnteredView; + public TextView userEnteredTextView; + } + private class SearchAdapter extends SimpleCursorAdapter { + private static final int ROW_SEARCH = 0; + private static final int ROW_STANDARD = 1; + private static final int ROW_SUGGEST = 2; + + private static final int ROW_TYPE_COUNT = 3; + public SearchAdapter(Context context) { super(context, -1, null, new String[] {}, new int[] {}); } @Override - public View getView(int position, View convertView, ViewGroup parent) { - final TwoLinePageRow row; - if (convertView == null) { - row = (TwoLinePageRow) LayoutInflater.from(getActivity()).inflate(R.layout.home_item_row, null); - } else { - row = (TwoLinePageRow) convertView; + public int getItemViewType(int position) { + final int engine = getEngineIndex(position); + + if (engine == -1) { + return ROW_STANDARD; + } else if (engine == 0 && mSuggestionsEnabled) { + // Give suggestion views their own type to prevent them from + // sharing other recycled search engine views. Using other + // recycled views for the suggestion row can break animations + // (bug 815937). + return ROW_SUGGEST; + } + + return ROW_SEARCH; + } + + @Override + public int getViewTypeCount() { + // view can be either a standard awesomebar row, a search engine + // row, or a suggestion row + return ROW_TYPE_COUNT; + } + + @Override + public boolean isEnabled(int position) { + // If the suggestion row only contains one item (the user-entered + // query), allow the entire row to be clickable; clicking the row + // has the same effect as clicking the single suggestion. If the + // row contains multiple items, clicking the row will do nothing. + final int index = getEngineIndex(position); + if (index != -1) { + return mSearchEngines.get(index).suggestions.isEmpty(); + } + + return true; + } + + // Add the search engines to the number of reported results. + @Override + public int getCount() { + final int resultCount = super.getCount(); + + // Don't show search engines or suggestions if search field is empty + if (TextUtils.isEmpty(mSearchTerm)) { + return resultCount; } - final Cursor c = getCursor(); - if (!c.moveToPosition(position)) { - throw new IllegalStateException("Couldn't move cursor to position " + position); + return resultCount + mSearchEngines.size(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final int type = getItemViewType(position); + + if (type == ROW_SEARCH || type == ROW_SUGGEST) { + final SearchEngineRow row; + if (convertView == null) { + row = (SearchEngineRow) mInflater.inflate(R.layout.home_search_item_row, mList, false); + row.setOnUrlOpenListener(mUrlOpenListener); + row.setOnSearchListener(mSearchListener); + row.setOnEditSuggestionListener(mEditSuggestionListener); + } else { + row = (SearchEngineRow) convertView; + } + + row.setSearchTerm(mSearchTerm); + + final SearchEngine engine = mSearchEngines.get(getEngineIndex(position)); + row.updateFromSearchEngine(engine); + + return row; + } else { + final TwoLinePageRow row; + if (convertView == null) { + row = (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, mList, false); + } else { + row = (TwoLinePageRow) convertView; + } + + // Account for the search engines + position -= getSuggestEngineCount(); + + final Cursor c = getCursor(); + if (!c.moveToPosition(position)) { + throw new IllegalStateException("Couldn't move cursor to position " + position); + } + + row.updateFromCursor(c); + + // FIXME: show bookmark icon + + return row; + } + } + + private int getSuggestEngineCount() { + return (TextUtils.isEmpty(mSearchTerm) || mSuggestClient == null || !mSuggestionsEnabled) ? 0 : 1; + } + + private int getEngineIndex(int position) { + final int resultCount = super.getCount(); + final int suggestEngineCount = getSuggestEngineCount(); + + // Return suggest engine index + if (position < suggestEngineCount) { + return position; } - row.updateFromCursor(c); + // Not an engine + if (position - suggestEngineCount < resultCount) { + return -1; + } - // FIXME: show bookmark icon - - return row; + // Return search engine index + return position - resultCount; } } private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> { @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch(id) { case SEARCH_LOADER_ID: @@ -316,9 +664,26 @@ public class BrowserSearch extends Fragm break; case FAVICONS_LOADER_ID: // Do nothing break; } } } + + private class SuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> { + @Override + public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) { + return new SuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm); + } + + @Override + public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) { + setSuggestions(suggestions); + } + + @Override + public void onLoaderReset(Loader<ArrayList<String>> loader) { + setSuggestions(new ArrayList<String>()); + } + } } \ No newline at end of file
--- a/mobile/android/base/BrowserToolbar.java +++ b/mobile/android/base/BrowserToolbar.java @@ -1023,16 +1023,28 @@ public class BrowserToolbar implements T visible &= !(url == null || (url.startsWith("about:") && !url.equals("about:blank"))) && !isEditing(); if ((mShadow.getVisibility() == View.VISIBLE) != visible) { mShadow.setVisibility(visible ? View.VISIBLE : View.GONE); } } + public void onEditSuggestion(String suggestion) { + if (!isEditing()) { + return; + } + + mUrlEditText.setText(suggestion); + mUrlEditText.setSelection(mUrlEditText.getText().length()); + mUrlEditText.requestFocus(); + + showSoftInput(); + } + private void setTitle(CharSequence title) { mTitle.setText(title); mLayout.setContentDescription(title != null ? title : mTitle.getHint()); } // Sets the toolbar title according to the selected tab, obeying the mShowUrl prference. private void updateTitle() { Tab tab = Tabs.getInstance().getSelectedTab();
--- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -132,16 +132,17 @@ FENNEC_JAVA_FILES = \ PrivateDataPreference.java \ PrivateTab.java \ ProfileMigrator.java \ Prompt.java \ PromptInput.java \ PromptService.java \ Restarter.java \ SearchEngine.java \ + SearchEngineRow.java \ sqlite/ByteBufferInputStream.java \ sqlite/MatrixBlobCursor.java \ sqlite/SQLiteBridge.java \ sqlite/SQLiteBridgeException.java \ ReaderModeUtils.java \ RemoteTabs.java \ RobocopAPI.java \ ServiceNotificationClient.java \ @@ -449,31 +450,33 @@ RES_LAYOUT = \ res/layout/datetime_picker.xml \ res/layout/doorhangerpopup.xml \ res/layout/doorhanger.xml \ res/layout/doorhanger_button.xml \ res/layout/find_in_page_content.xml \ res/layout/font_size_preference.xml \ res/layout/gecko_app.xml \ res/layout/home_item_row.xml \ + res/layout/home_search_item_row.xml \ res/layout/web_app.xml \ res/layout/launch_app_list.xml \ res/layout/launch_app_listitem.xml \ res/layout/menu_action_bar.xml \ res/layout/menu_popup.xml \ res/layout/notification_icon_text.xml \ res/layout/notification_progress.xml \ res/layout/notification_progress_text.xml \ res/layout/site_setting_item.xml \ res/layout/site_setting_title.xml \ res/layout/setup_screen.xml \ res/layout/shared_ui_components.xml \ res/layout/site_identity_popup.xml \ res/layout/remote_tabs_child.xml \ res/layout/remote_tabs_group.xml \ + res/layout/search_engine_row.xml \ res/layout/tabs_panel.xml \ res/layout/tabs_counter.xml \ res/layout/tabs_panel_header.xml \ res/layout/tabs_panel_indicator.xml \ res/layout/tabs_item_cell.xml \ res/layout/tabs_item_row.xml \ res/layout/text_selection_handles.xml \ res/layout/two_line_page_row.xml \
new file mode 100644 --- /dev/null +++ b/mobile/android/base/SearchEngineRow.java @@ -0,0 +1,173 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import org.mozilla.gecko.BrowserSearch.OnEditSuggestionListener; +import org.mozilla.gecko.BrowserSearch.OnSearchListener; +import org.mozilla.gecko.BrowserSearch.OnUrlOpenListener; +import org.mozilla.gecko.util.StringUtils; +import org.mozilla.gecko.widget.FaviconView; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +class SearchEngineRow extends AnimatedHeightLayout { + + // Inner views + private final FlowLayout mSuggestionView; + private final FaviconView mIconView; + private final LinearLayout mUserEnteredView; + private final TextView mUserEnteredTextView; + + // Inflater used when updating from suggestions + private final LayoutInflater mInflater; + + // Search engine associated with this view + private SearchEngine mSearchEngine; + + // Event listeners for suggestion views + private final OnClickListener mClickListener; + private final OnLongClickListener mLongClickListener; + + // On URL open listener + private OnUrlOpenListener mUrlOpenListener; + + // On search listener + private OnSearchListener mSearchListener; + + // On edit suggestion listener + private OnEditSuggestionListener mEditSuggestionListener; + + public SearchEngineRow(Context context) { + this(context, null); + } + + public SearchEngineRow(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchEngineRow(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + final String suggestion = getSuggestionTextFromView(v); + + // If we're not clicking the user-entered view (the first suggestion item) + // and the search matches a URL pattern, go to that URL. Otherwise, do a + // search for the term. + if (v != mUserEnteredView && !StringUtils.isSearchQuery(suggestion, false)) { + if (mUrlOpenListener != null) { + mUrlOpenListener.onUrlOpen(suggestion); + } + } else if (mSearchListener != null) { + mSearchListener.onSearch(mSearchEngine, suggestion); + } + } + }; + + mLongClickListener = new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (mEditSuggestionListener != null) { + final String suggestion = getSuggestionTextFromView(v); + mEditSuggestionListener.onEditSuggestion(suggestion); + return true; + } + + return false; + } + }; + + mInflater = LayoutInflater.from(context); + mInflater.inflate(R.layout.search_engine_row, this); + + mSuggestionView = (FlowLayout) findViewById(R.id.suggestion_layout); + mIconView = (FaviconView) findViewById(R.id.suggestion_icon); + + // User-entered search term is first suggestion + mUserEnteredView = (LinearLayout) findViewById(R.id.suggestion_user_entered); + mUserEnteredView.setOnClickListener(mClickListener); + + mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text); + } + + private String getSuggestionTextFromView(View v) { + final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); + return suggestionText.getText().toString(); + } + + private void setSuggestionOnView(View v, String suggestion) { + final TextView suggestionText = (TextView) v.findViewById(R.id.suggestion_text); + suggestionText.setText(suggestion); + } + + public void setSearchTerm(String searchTerm) { + mUserEnteredTextView.setText(searchTerm); + } + + public void setOnUrlOpenListener(OnUrlOpenListener listener) { + mUrlOpenListener = listener; + } + + public void setOnSearchListener(OnSearchListener listener) { + mSearchListener = listener; + } + + public void setOnEditSuggestionListener(OnEditSuggestionListener listener) { + mEditSuggestionListener = listener; + } + + public void updateFromSearchEngine(SearchEngine searchEngine) { + // Update search engine reference + mSearchEngine = searchEngine; + + // Set the search engine icon (e.g., Google) for the row + mIconView.updateImage(mSearchEngine.icon, mSearchEngine.name); + + // Add additional suggestions given by this engine + final int recycledSuggestionCount = mSuggestionView.getChildCount(); + final int suggestionCount = mSearchEngine.suggestions.size(); + + for (int i = 0; i < suggestionCount; i++) { + final View suggestionItem; + + // Reuse suggestion views from recycled view, if possible + if (i + 1 < recycledSuggestionCount) { + suggestionItem = mSuggestionView.getChildAt(i + 1); + suggestionItem.setVisibility(View.VISIBLE); + } else { + suggestionItem = mInflater.inflate(R.layout.awesomebar_suggestion_item, null); + + suggestionItem.setOnClickListener(mClickListener); + suggestionItem.setOnLongClickListener(mLongClickListener); + + final ImageView magnifier = + (ImageView) suggestionItem.findViewById(R.id.suggestion_magnifier); + magnifier.setVisibility(View.GONE); + + mSuggestionView.addView(suggestionItem); + } + + final String suggestion = mSearchEngine.suggestions.get(i); + setSuggestionOnView(suggestionItem, suggestion); + } + + // Hide extra suggestions that have been recycled + for (int i = suggestionCount + 1; i < recycledSuggestionCount; i++) { + mSuggestionView.getChildAt(i).setVisibility(View.GONE); + } + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/base/resources/layout/home_search_item_row.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<org.mozilla.gecko.SearchEngineRow xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/page_row_height" + android:padding="7dp"/>
new file mode 100644 --- /dev/null +++ b/mobile/android/base/resources/layout/search_engine_row.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + + <Gecko.FaviconView android:id="@+id/suggestion_icon" + android:layout_width="@dimen/favicon_bg" + android:layout_height="@dimen/favicon_bg" + android:layout_marginLeft="6dip" + android:layout_marginRight="6dip" + android:layout_centerVertical="true" + android:minWidth="@dimen/favicon_bg" + android:minHeight="@dimen/favicon_bg"/> + + <org.mozilla.gecko.FlowLayout android:id="@+id/suggestion_layout" + android:layout_toRightOf="@id/suggestion_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <include layout="@layout/awesomebar_suggestion_item" + android:id="@+id/suggestion_user_entered"/> + + </org.mozilla.gecko.FlowLayout> + +</merge> \ No newline at end of file