Bug 877870 - Initial implementation of new browser search fragment (r=bnicholson)
authorLucas Rocha <lucasr@mozilla.com>
Tue, 11 Jun 2013 17:57:45 +0100
changeset 143319 68cd82adf04026606f618ded50d82b160eb13fa1
parent 143318 0807f3d71b68b3ed1b03bcd2b2a67feb298164f1
child 143320 72e003d38d009b4630f9cec45bc98e70f2d5767c
push id25130
push userlrocha@mozilla.com
push dateWed, 21 Aug 2013 09:41:27 +0000
treeherdermozilla-central@b2486721572e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbnicholson
bugs877870
milestone24.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 877870 - Initial implementation of new browser search fragment (r=bnicholson)
mobile/android/base/BrowserApp.java
mobile/android/base/BrowserSearch.java
mobile/android/base/Makefile.in
mobile/android/base/SimpleCursorLoader.java
mobile/android/base/resources/layout/bookmark_item_row.xml
mobile/android/base/resources/layout/gecko_app.xml
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -69,31 +69,36 @@ 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.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";
 
     private static final int TABS_ANIMATION_DURATION = 450;
 
     private static final int READER_ADD_SUCCESS = 0;
     private static final int READER_ADD_FAILED = 1;
     private static final int READER_ADD_DUPLICATE = 2;
 
     private static final String STATE_DYNAMIC_TOOLBAR_ENABLED = "dynamic_toolbar";
 
+    private static final String BROWSER_SEARCH_TAG = "browser_search";
+    private BrowserSearch mBrowserSearch;
+    private View mBrowserSearchContainer;
+
     public static BrowserToolbar mBrowserToolbar;
     private HomePager mHomePager;
     protected Telemetry.Timer mAboutHomeStartupTimer = null;
 
     private static final int ADDON_MENU_OFFSET = 1000;
     private class MenuItemInfo {
         public int id;
         public String label;
@@ -376,16 +381,23 @@ abstract public class BrowserApp extends
                     }
                 }
                 return false;
             }
         });
 
         mHomePager = (HomePager) findViewById(R.id.home_pager);
 
+        mBrowserSearchContainer = findViewById(R.id.search_container);
+        mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG);
+        if (mBrowserSearch == null) {
+            mBrowserSearch = BrowserSearch.newInstance();
+            mBrowserSearch.setUserVisibleHint(false);
+        }
+
         mBrowserToolbar = new BrowserToolbar(this);
         mBrowserToolbar.from(actionBar);
 
         mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() {
             public void onActivate() {
                 enterEditingMode(EditingTarget.CURRENT_TAB);
             }
         });
@@ -1120,16 +1132,18 @@ abstract public class BrowserApp extends
         mBrowserToolbar.setProgressVisibility(true);
 
         int flags = Tabs.LOADURL_NONE;
         if (mBrowserToolbar.getEditingTarget() == EditingTarget.NEW_TAB) {
             flags |= Tabs.LOADURL_NEW_TAB;
         }
 
         Tabs.getInstance().loadUrl(url, flags);
+
+        hideBrowserSearch();
         mBrowserToolbar.cancelEdit();
     }
 
     /* Favicon methods */
     private void loadFavicon(final Tab tab) {
         maybeCancelFaviconLoad(tab);
 
         long id = Favicons.getInstance().loadFavicon(tab.getURL(), tab.getFaviconURL(), !tab.isPrivate(),
@@ -1202,16 +1216,17 @@ abstract public class BrowserApp extends
 
     void commitEditingMode(EditingTarget target) {
         if (!mBrowserToolbar.isEditing()) {
             return;
         }
 
         final String url = mBrowserToolbar.commitEdit();
         animateHideHomePager();
+        hideBrowserSearch();
 
         int flags = Tabs.LOADURL_USER_ENTERED;
         if (target == EditingTarget.NEW_TAB) {
             flags |= Tabs.LOADURL_NEW_TAB;
         }
 
         if (!TextUtils.isEmpty(url)) {
             Tabs.getInstance().loadUrl(url, flags);
@@ -1220,22 +1235,28 @@ abstract public class BrowserApp extends
 
     boolean dismissEditingMode() {
         if (!mBrowserToolbar.isEditing()) {
             return false;
         }
 
         mBrowserToolbar.cancelEdit();
         animateHideHomePager();
+        hideBrowserSearch();
 
         return true;
     }
 
     void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
-        // FIXME: implement actual awesomebar search
+        if (TextUtils.isEmpty(searchTerm)) {
+            hideBrowserSearch();
+        } else {
+            showBrowserSearch();
+            mBrowserSearch.filter(searchTerm);
+        }
     }
 
     private void animateShowHomePager() {
         showHomePagerWithAnimation(true);
     }
 
      private void showHomePager() {
         showHomePagerWithAnimation(false);
@@ -1284,16 +1305,40 @@ abstract public class BrowserApp extends
 
         mBrowserToolbar.setShadowVisibility(true);
         mBrowserToolbar.setNextFocusDownId(R.id.layer_view);
 
         // Refresh toolbar height to possibly restore the toolbar padding
         refreshToolbarHeight();
     }
 
+    private void showBrowserSearch() {
+        if (mBrowserSearch.getUserVisibleHint()) {
+            return;
+        }
+
+        mBrowserSearchContainer.setVisibility(View.VISIBLE);
+
+        getSupportFragmentManager().beginTransaction()
+                .add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
+        mBrowserSearch.setUserVisibleHint(true);
+    }
+
+    private void hideBrowserSearch() {
+        if (!mBrowserSearch.getUserVisibleHint()) {
+            return;
+        }
+
+        mBrowserSearchContainer.setVisibility(View.INVISIBLE);
+
+        getSupportFragmentManager().beginTransaction()
+                .remove(mBrowserSearch).commitAllowingStateLoss();
+        mBrowserSearch.setUserVisibleHint(false);
+    }
+
     private class HideTabsTouchListener implements TouchEventInterceptor {
         private boolean mIsHidingTabs = false;
 
         @Override
         public boolean onInterceptTouchEvent(View view, MotionEvent event) {
             // We need to account for scroll state for the touched view otherwise
             // tapping on an "empty" part of the view will still be considered a
             // valid touch event.
@@ -1768,17 +1813,17 @@ abstract public class BrowserApp extends
             public void onPostExecute(String url) {
                 // Don't bother sending a message if there is no URL.
                 if (url.length() > 0)
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:LastUrl", url));
             }
         }).execute();
     }
 
-    // HomePager.OnUrlOpenListener
+    // (HomePager|BrowserSearch).OnUrlOpenListener
     @Override
     public void onUrlOpen(String url) {
         openUrl(url);
     }
 
     @Override
     public int getLayout() { return R.layout.gecko_app; }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/BrowserSearch.java
@@ -0,0 +1,202 @@
+/* -*- 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.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserDB.URLColumns;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.home.TwoLinePageRow;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+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.Loader;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+
+/**
+ * Fragment that displays frecency search results in a ListView.
+ */
+public class BrowserSearch extends Fragment implements LoaderCallbacks<Cursor>,
+                                                       AdapterView.OnItemClickListener {
+    // Cursor loader ID for search query
+    private static final int SEARCH_LOADER_ID = 0;
+
+    // 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;
+
+    // On URL open listener
+    private OnUrlOpenListener mUrlOpenListener;
+
+    public interface OnUrlOpenListener {
+        public void onUrlOpen(String url);
+    }
+
+    public static BrowserSearch newInstance() {
+        return new BrowserSearch();
+    }
+
+    public BrowserSearch() {
+        mSearchTerm = "";
+    }
+
+    @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");
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+
+        mUrlOpenListener = 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;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mList.setOnItemClickListener(this);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        // Intialize the search adapter
+        mAdapter = new SearchAdapter(getActivity());
+        mList.setAdapter(mAdapter);
+
+        // Reconnect to the loader only if present
+        getLoaderManager().initLoader(SEARCH_LOADER_ID, null, this);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        return new SearchCursorLoader(getActivity(), mSearchTerm);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
+        mAdapter.swapCursor(c);
+
+        // FIXME: do extra UI bits here
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        mAdapter.swapCursor(null);
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        final Cursor c = mAdapter.getCursor();
+        if (c == null || !c.moveToPosition(position)) {
+            return;
+        }
+
+        final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
+        mUrlOpenListener.onUrlOpen(url);
+    }
+
+    public void filter(String searchTerm) {
+        if (TextUtils.isEmpty(searchTerm)) {
+            return;
+        }
+
+        if (TextUtils.equals(mSearchTerm, searchTerm)) {
+            return;
+        }
+
+        mSearchTerm = searchTerm;
+
+        if (isVisible()) {
+            getLoaderManager().restartLoader(SEARCH_LOADER_ID, null, this);
+        }
+    }
+
+    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
+        private final String mSearchTerm;
+
+        public SearchCursorLoader(Context context, String searchTerm) {
+            super(context);
+            mSearchTerm = searchTerm;
+        }
+
+        @Override
+        public Cursor loadCursor() {
+            if (TextUtils.isEmpty(mSearchTerm)) {
+                return null;
+            }
+
+            final ContentResolver cr = getContext().getContentResolver();
+            return BrowserDB.filter(cr, mSearchTerm, SEARCH_LIMIT);
+        }
+    }
+
+    private class SearchAdapter extends SimpleCursorAdapter {
+        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;
+            }
+
+            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;
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -61,16 +61,17 @@ FENNEC_JAVA_FILES = \
   animation/Rotate3DAnimation.java \
   animation/ViewHelper.java \
   awesomebar/AwesomeBarTab.java \
   awesomebar/AllPagesTab.java \
   awesomebar/BookmarksTab.java \
   awesomebar/HistoryTab.java \
   BackButton.java \
   BrowserApp.java \
+  BrowserSearch.java \
   BrowserToolbar.java \
   BrowserToolbarBackground.java \
   BrowserToolbarLayout.java \
   CameraImageResultHandler.java \
   CameraVideoResultHandler.java \
   CanvasDelegate.java \
   CheckableLinearLayout.java \
   ClickableWhenDisabledEditText.java \
@@ -143,16 +144,17 @@ FENNEC_JAVA_FILES = \
   ReaderModeUtils.java \
   RemoteTabs.java \
   RobocopAPI.java \
   ServiceNotificationClient.java \
   SessionParser.java \
   SetupScreen.java \
   ShapedButton.java \
   SharedPreferencesHelper.java \
+  SimpleCursorLoader.java \
   SiteIdentityPopup.java \
   SmsManager.java \
   SuggestClient.java \
   SurfaceBits.java \
   SyncPreference.java \
   Tab.java \
   TabCounter.java \
   Tabs.java \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/SimpleCursorLoader.java
@@ -0,0 +1,131 @@
+/*
+ * This is an adapted version of Android's original CursorLoader
+ * without all the ContentProvider-specific bits.
+ *
+ * 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 org.mozilla.gecko;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.support.v4.content.AsyncTaskLoader;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+public abstract class SimpleCursorLoader extends AsyncTaskLoader<Cursor> {
+    final ForceLoadContentObserver mObserver;
+    Cursor mCursor;
+
+    public SimpleCursorLoader(Context context) {
+        super(context);
+        mObserver = new ForceLoadContentObserver();
+    }
+
+    /**
+     * Loads the target cursor for this loader. This method is called
+     * on a worker thread.
+     */
+    protected abstract Cursor loadCursor();
+
+    /* Runs on a worker thread */
+    @Override
+    public Cursor loadInBackground() {
+        Cursor cursor = loadCursor();
+
+        if (cursor != null) {
+            // Ensure the cursor window is filled
+            cursor.getCount();
+            cursor.registerContentObserver(mObserver);
+        }
+
+        return cursor;
+    }
+
+    /* Runs on the UI thread */
+    @Override
+    public void deliverResult(Cursor cursor) {
+        if (isReset()) {
+            // An async query came in while the loader is stopped
+            if (cursor != null) {
+                cursor.close();
+            }
+
+            return;
+        }
+
+        Cursor oldCursor = mCursor;
+        mCursor = cursor;
+
+        if (isStarted()) {
+            super.deliverResult(cursor);
+        }
+
+        if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
+            oldCursor.close();
+        }
+    }
+
+    /**
+     * Starts an asynchronous load of the list data. When the result is ready the callbacks
+     * will be called on the UI thread. If a previous load has been completed and is still valid
+     * the result may be passed to the callbacks immediately.
+     *
+     * Must be called from the UI thread
+     */
+    @Override
+    protected void onStartLoading() {
+        if (mCursor != null) {
+            deliverResult(mCursor);
+        }
+
+        if (takeContentChanged() || mCursor == null) {
+            forceLoad();
+        }
+    }
+
+    /**
+     * Must be called from the UI thread
+     */
+    @Override
+    protected void onStopLoading() {
+        // Attempt to cancel the current load task if possible.
+        cancelLoad();
+    }
+
+    @Override
+    public void onCanceled(Cursor cursor) {
+        if (cursor != null && !cursor.isClosed()) {
+            cursor.close();
+        }
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+
+        // Ensure the loader is stopped
+        onStopLoading();
+
+        if (mCursor != null && !mCursor.isClosed()) {
+            mCursor.close();
+        }
+
+        mCursor = null;
+    }
+}
\ No newline at end of file
deleted file mode 100644
--- a/mobile/android/base/resources/layout/bookmark_item_row.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?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.home.TwoLinePageRow xmlns:android="http://schemas.android.com/apk/res/android"
-                                       android:layout_width="fill_parent"
-                                       android:layout_height="@dimen/page_row_height"
-                                       android:minHeight="@dimen/page_row_height"/>
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -50,11 +50,19 @@
                                          android:layout_height="wrap_content"
                                          android:layout_alignParentBottom="true"
                                          style="@style/FindBar"
                                          android:visibility="gone"/>
 
         <include layout="@layout/browser_toolbar"
                  android:layout_width="fill_parent"
                  android:layout_height="@dimen/browser_toolbar_height"/>
+
+        <FrameLayout android:id="@+id/search_container"
+                     android:layout_width="fill_parent"
+                     android:layout_height="fill_parent"
+                     android:layout_below="@id/browser_toolbar"
+                     android:background="@android:color/white"
+                     android:visibility="invisible"/>
+
     </view>
 
 </RelativeLayout>