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 151303 68cd82adf04026606f618ded50d82b160eb13fa1
parent 151302 0807f3d71b68b3ed1b03bcd2b2a67feb298164f1
child 151304 72e003d38d009b4630f9cec45bc98e70f2d5767c
push idunknown
push userunknown
push dateunknown
reviewersbnicholson
bugs877870
milestone24.0a1
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>