Bug 1137483 - Add search engine bar to the bottom of BrowserSearch. r=liuche
☠☠ backed out by 86203ac87a08 ☠ ☠
authorMichael Comella <michael.l.comella@gmail.com>
Tue, 21 Apr 2015 16:25:33 -0700
changeset 274013 0602fff3681db69f5dc2d6ef7745f4b8fabfa5f6
parent 274012 ba4ae6b6dcfcbe87367047b586cc49ebd2a72121
child 274014 399980ce108a47e41be77480eacf731a39e91ada
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersliuche
bugs1137483
milestone40.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
Bug 1137483 - Add search engine bar to the bottom of BrowserSearch. r=liuche
mobile/android/base/home/BrowserSearch.java
mobile/android/base/home/SearchEngineBar.java
mobile/android/base/moz.build
mobile/android/base/resources/color/pressed_about_page_header_grey.xml
mobile/android/base/resources/layout/browser_search.xml
mobile/android/base/resources/layout/search_engine_bar_item.xml
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -60,17 +60,18 @@ import android.widget.AdapterView;
 import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.TextView;
 
 /**
  * Fragment that displays frecency search results in a ListView.
  */
 public class BrowserSearch extends HomeFragment
-                           implements GeckoEventListener {
+                           implements GeckoEventListener,
+                                      SearchEngineBar.OnSearchBarClickListener {
 
     @RobocopTarget
     public interface SuggestClientFactory {
         public SuggestClient getSuggestClient(Context context, String template, int timeout, int max);
     }
 
     @RobocopTarget
     public static class DefaultSuggestClientFactory implements SuggestClientFactory {
@@ -119,16 +120,19 @@ public class BrowserSearch extends HomeF
     private SearchAdapter mAdapter;
 
     // The view shown by the fragment
     private LinearLayout mView;
 
     // The list showing search results
     private HomeListView mList;
 
+    // The bar on the bottom of the screen displaying search engine options.
+    private SearchEngineBar mSearchEngineBar;
+
     // Client that performs search suggestion queries.
     // Public for testing.
     @RobocopTarget
     public volatile SuggestClient mSuggestClient;
 
     // List of search engines from Gecko.
     // Do not mutate this list.
     // Access to this member must only occur from the UI thread.
@@ -263,27 +267,31 @@ public class BrowserSearch extends HomeF
     }
 
     @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.
         mView = (LinearLayout) inflater.inflate(R.layout.browser_search, container, false);
         mList = (HomeListView) mView.findViewById(R.id.home_list_view);
+        mSearchEngineBar = (SearchEngineBar) mView.findViewById(R.id.search_engine_bar);
 
         return mView;
     }
 
     @Override
     public void onDestroyView() {
         super.onDestroyView();
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
             "SearchEngines:Data");
 
+        mSearchEngineBar.setAdapter(null);
+        mSearchEngineBar = null;
+
         mList.setAdapter(null);
         mList = null;
 
         mView = null;
         mSuggestionsOptInPrompt = null;
         mSuggestClient = null;
     }
 
@@ -345,16 +353,22 @@ public class BrowserSearch extends HomeF
                 }
                 return false;
             }
         });
 
         registerForContextMenu(mList);
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "SearchEngines:Data");
+
+        // If the view backed by this Fragment is being recreated, we will not receive
+        // a new search engine data event so refresh the new search engine bar's data
+        // & Views with the data we have.
+        mSearchEngineBar.setSearchEngines(mSearchEngines);
+        mSearchEngineBar.setOnSearchBarClickListener(this);
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
         // Initialize the search adapter
         mAdapter = new SearchAdapter(getActivity());
@@ -582,28 +596,35 @@ public class BrowserSearch extends HomeF
 
             mSearchEngines = Collections.unmodifiableList(searchEngines);
             mLastLocale = Locale.getDefault();
 
             if (mAdapter != null) {
                 mAdapter.notifyDataSetChanged();
             }
 
+            mSearchEngineBar.setSearchEngines(mSearchEngines);
+
             // Show suggestions opt-in prompt only if suggestions are not enabled yet,
             // user hasn't been prompted and we're not on a private browsing tab.
             if (!mSuggestionsEnabled && !suggestionsPrompted && mSuggestClient != null) {
                 showSuggestionsOptIn();
             }
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error getting search engine JSON", e);
         }
 
         filterSuggestions();
     }
 
+    @Override
+    public void onSearchBarClickListener(final SearchEngine searchEngine) {
+        mSearchListener.onSearch(searchEngine, mSearchTerm);
+    }
+
     private void maybeSetSuggestClient(final String suggestTemplate, final boolean isPrivate) {
         if (mSuggestClient != null || isPrivate) {
             return;
         }
 
         mSuggestClient = sSuggestClientFactory.getSuggestClient(getActivity(), suggestTemplate, SUGGESTION_TIMEOUT, SUGGESTION_MAX);
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/SearchEngineBar.java
@@ -0,0 +1,115 @@
+/* -*- 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.home;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.widget.FaviconView;
+import org.mozilla.gecko.widget.TwoWayView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchEngineBar extends TwoWayView
+                             implements AdapterView.OnItemClickListener {
+    private static final String LOGTAG = "Gecko" + SearchEngineBar.class.getSimpleName();
+
+    public interface OnSearchBarClickListener {
+        public void onSearchBarClickListener(SearchEngine searchEngine);
+    }
+
+    private final SearchEngineAdapter adapter;
+    private OnSearchBarClickListener onSearchBarClickListener;
+
+    private final Paint dividerPaint;
+
+    public SearchEngineBar(final Context context, final AttributeSet attrs) {
+        super(context, attrs);
+
+        adapter = new SearchEngineAdapter();
+        setAdapter(adapter);
+        setOnItemClickListener(this);
+
+        dividerPaint = new Paint();
+        dividerPaint.setColor(getResources().getColor(R.color.divider_light));
+    }
+
+    @Override
+    public void onDraw(final Canvas canvas) {
+        super.onDraw(canvas);
+
+        canvas.drawLine(0, 0, getWidth(), 0, dividerPaint);
+    }
+
+    @Override
+    public void onItemClick(final AdapterView<?> parent, final View view, final int position,
+            final long id) {
+        if (onSearchBarClickListener == null) {
+            throw new IllegalStateException(
+                    OnSearchBarClickListener.class.getSimpleName() + " is not initialized");
+        }
+
+        final SearchEngine searchEngine = adapter.getItem(position);
+        onSearchBarClickListener.onSearchBarClickListener(searchEngine);
+    }
+
+    protected void setOnSearchBarClickListener(final OnSearchBarClickListener listener) {
+        onSearchBarClickListener = listener;
+    }
+
+    protected void setSearchEngines(final List<SearchEngine> searchEngines) {
+        adapter.setSearchEngines(searchEngines);
+    }
+
+    public class SearchEngineAdapter extends BaseAdapter {
+        List<SearchEngine> searchEngines = new ArrayList<>();
+
+        public void setSearchEngines(final List<SearchEngine> searchEngines) {
+            this.searchEngines = searchEngines;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getCount() {
+            return searchEngines.size();
+        }
+
+        @Override
+        public SearchEngine getItem(final int position) {
+            return searchEngines.get(position);
+        }
+
+        @Override
+        public long getItemId(final int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(final int position, final View convertView, final ViewGroup parent) {
+            final View view;
+            if (convertView == null) {
+                view = LayoutInflater.from(getContext()).inflate(R.layout.search_engine_bar_item, parent, false);
+            } else {
+                view = convertView;
+            }
+
+            final FaviconView faviconView = (FaviconView) view.findViewById(R.id.search_engine_icon);
+            final SearchEngine searchEngine = searchEngines.get(position);
+            faviconView.updateAndScaleImage(searchEngine.getIcon(), searchEngine.getEngineIdentifier());
+
+            return view;
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -335,16 +335,17 @@ gbjar.sources += [
     'home/RecentTabsPanel.java',
     'home/RemoteTabsBaseFragment.java',
     'home/RemoteTabsExpandableListFragment.java',
     'home/RemoteTabsExpandableListState.java',
     'home/RemoteTabsPanel.java',
     'home/RemoteTabsSplitPlaneFragment.java',
     'home/RemoteTabsStaticFragment.java',
     'home/SearchEngine.java',
+    'home/SearchEngineBar.java',
     'home/SearchEngineRow.java',
     'home/SearchLoader.java',
     'home/SimpleCursorLoader.java',
     'home/TabMenuStrip.java',
     'home/TabMenuStripLayout.java',
     'home/TopSitesGridItemView.java',
     'home/TopSitesGridView.java',
     'home/TopSitesPanel.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/color/pressed_about_page_header_grey.xml
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:state_pressed="true"
+          android:drawable="@color/about_page_header_grey" />
+
+    <item android:drawable="@android:color/transparent"/>
+
+</selector>
--- a/mobile/android/base/resources/layout/browser_search.xml
+++ b/mobile/android/base/resources/layout/browser_search.xml
@@ -14,9 +14,29 @@
               android:layout="@layout/home_suggestion_prompt" />
 
     <view class="org.mozilla.gecko.home.BrowserSearch$HomeSearchListView"
             android:id="@+id/home_list_view"
             android:layout_width="match_parent"
             android:layout_height="0dp"
             android:layout_weight="1" />
 
+    <!-- The desired layout_height is 48dp. We add paddingTop so we have
+         space to dynamically draw the divider in onDraw. Preferably, we'd
+         wrap_content on the inner layout, but TwoWayView ignores wrap_content. :(
+
+         Note: the layout_height value is shared with the inner layout
+         (search_engine_bar_item at the time of this writing).
+
+         listSelector is too slow for showing pressed state
+         so we set the pressed colors on the child. -->
+    <org.mozilla.gecko.home.SearchEngineBar
+          android:id="@+id/search_engine_bar"
+          android:layout_width="match_parent"
+          android:layout_height="49dp"
+          android:paddingTop="1dp"
+          android:orientation="horizontal"
+          android:background="#fff"
+          android:choiceMode="singleChoice"
+          android:listSelector="@android:color/transparent"
+          android:cacheColorHint="@android:color/transparent"/>
+
 </LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/search_engine_bar_item.xml
@@ -0,0 +1,26 @@
+<?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/. -->
+
+<!-- TwoWayView doesn't let us set the margin around items (except as
+     gecko:itemMargin, but that doesn't increase the hit area) so we
+     have to surround the main View by a ViewGroup to create a pressable margin.
+
+     Note: the layout_height values are shared with the parent
+     View (browser_search at the time of this writing). -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="48dp"
+    android:layout_width="72dp"
+    android:background="@color/pressed_about_page_header_grey">
+
+    <!-- Width & height are set to make the Favicons as sharp as possible
+         based on asset size. -->
+    <org.mozilla.gecko.widget.FaviconView
+        android:id="@+id/search_engine_icon"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:layout_gravity="center"/>
+
+</FrameLayout>